Node.js Runtime

īŋŊ Everything You Learned Still Applies!

✅

Great News: JavaScript is JavaScript!

Everything you learned in the JS Refresher section applies to Node.js:

  • ✓ Variables, types, operators
  • ✓ Functions, arrow functions, callbacks
  • ✓ Objects, arrays, destructuring
  • ✓ Promises, async/await
  • ✓ Classes, this, spread operator
  • ✓ Map, filter, reduce, and all array methods
âš ī¸ What's NOT Available in Node.js

Browser-specific APIs don't exist in Node.js: document, window, alert(), localStorage, etc.

Node.js runs outside the browser, so it has its own APIs for things like file systems, networking, and process management.

đŸŽ¯ What is Node.js?

Node.js is a JavaScript runtime built on Chrome's V8 engine. It allows you to run JavaScript outside the browser — on your computer, on a server, anywhere! This means JavaScript is no longer limited to making web pages interactive; you can now build:

đŸ–Ĩī¸
Desktop Applications
Build apps like VS Code, Slack, Discord
🚀
Try Node.js Now!
Free online playground – no installation
→
🌐
Web Servers & APIs
Backend services, REST APIs, GraphQL
🤖
Bots & Automation
Discord bots, scrapers, task automation
đŸ’ģ
CLI Tools
Command-line applications, scripts

đŸ’ģ Live Example: JS Refresher Code Running in Node.js

Here's a comprehensive example showcasing that all the JavaScript you learned works perfectly in Node.js:

// ✅ ALL of this JavaScript works the same in Node.js!

// đŸ“Ļ Variables & Arrow Functions
const greet = (name) => `Hello, ${name}!`;
console.log(greet('Node.js')); // Hello, Node.js!

// 🎨 Template Literals
const version = 20;
console.log(`Running on Node.js v${version}`);

// đŸ”ĸ Arrays & Modern Methods (map, filter, reduce)
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log('Doubled:', doubled);  // [2, 4, 6, 8, 10]
console.log('Evens:', evens);      // [2, 4]
console.log('Sum:', sum);          // 15

// đŸ“Ļ Objects & Destructuring
const user = {
  name: 'Alice',
  age: 25,
  skills: ['JavaScript', 'Node.js', 'React']
};
const { name, skills } = user;
console.log(`${name} knows: ${skills.join(', ')}`);

// 🔄 Spread Operator
const moreSkills = [...skills, 'Express', 'MongoDB'];
console.log('Updated skills:', moreSkills);

// ⚡ Promises & Async/Await
const fetchData = (delay = 1000) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ message: 'Data loaded!', timestamp: Date.now() });
    }, delay);
  });
};

async function main() {
  console.log('Fetching data...');
  const data = await fetchData(500);
  console.log('✅', data.message);
  
  // đŸŽ¯ Multiple async operations
  const results = await Promise.all([
    fetchData(300),
    fetchData(200),
    fetchData(100)
  ]);
  console.log(`Loaded ${results.length} items`);
}

main().catch(err => console.error('Error:', err));

// đŸ—ī¸ Classes (ES6+)
class TodoList {
  constructor() {
    this.todos = [];
  }
  
  add(task) {
    this.todos.push({ task, done: false });
    return this;
  }
  
  complete(index) {
    if (this.todos[index]) {
      this.todos[index].done = true;
    }
    return this;
  }
  
  list() {
    return this.todos;
  }
}

const myTodos = new TodoList();
myTodos.add('Learn Node.js').add('Build an API').complete(0);
console.log('My todos:', myTodos.list());

// đŸŽ¯ Try/Catch Error Handling
try {
  const result = JSON.parse('{"valid": "json"}');
  console.log('Parsed:', result);
} catch (error) {
  console.error('Parse error:', error.message);
}
🎉

Same Language, Different Environment

You already know JavaScript! Node.js simply gives you a new runtime environment with new APIs. The core language (variables, functions, promises, classes, etc.) remains 100% identical. 🚀

đŸ“Ĩ Installation & Setup

1ī¸âƒŖ Download Node.js

Go to nodejs.org and download the LTS (Long Term Support) version. This is the recommended, stable version for most users.

💡 Installation Tips
  • Windows: Download the .msi installer and run it
  • macOS: Download the .pkg installer or use Homebrew: brew install node
  • Linux: Use your package manager or download from nodejs.org

2ī¸âƒŖ Verify Installation

After installation, open your terminal/command prompt and verify Node.js is installed:

# Check Node.js version
node --version
# Output: v20.x.x (or similar)

# Check npm (Node Package Manager) version
npm --version
# Output: 10.x.x (or similar)
â„šī¸ What is npm?

npm (Node Package Manager) comes bundled with Node.js. It's the tool you use to install third-party packages (like Express, React, etc.). Think of it as an app store for JavaScript libraries!

Similar tools in other ecosystems: Maven/Gradle (Java), pip (Python), Composer (PHP), NuGet (.NET), Cargo (Rust), RubyGems (Ruby), Go Modules (Go).

â–ļī¸ Running Node.js

đŸ–Ĩī¸ Node.js REPL (Interactive Console)

The REPL (Read-Eval-Print Loop) is an interactive JavaScript console where you can test code quickly. Just type node in your terminal:

# Start the Node.js REPL
node

# Now you can run JavaScript:
> 2 + 2
4
> const name = "Node.js"
undefined
> console.log(`Hello, ${name}!`)
Hello, Node.js!
undefined
> .exit   // Exit the REPL (or press Ctrl+C twice)

📄 Executing a JavaScript File

This is where the magic happens! You can now run JavaScript files without a browser. Create a file called app.js:

// app.js
console.log('Hello from Node.js!');
console.log('JavaScript running outside the browser! 🚀');

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(`Sum: ${sum}`);

Run it with Node.js:

node app.js

Output:

# Hello from Node.js!
# JavaScript running outside the browser! 🚀
# Sum: 15
💡 File Path Options

You have three ways to run your JavaScript file:

1. Navigate to the file location:
cd /path/to/your/folder
node app.js
2. Use a relative path:
node ./subfolder/app.js or node ../other-folder/app.js
3. Use an absolute (full) path:
node /Users/yourname/projects/app.js (Mac/Linux)
node C:\Users\yourname\projects\app.js (Windows)
🎉

This is a Game Changer!

You just ran JavaScript without opening a browser! No HTML, no <script> tags. Just pure JavaScript executing on your machine. This is the foundation for building servers, CLI tools, and much more.

đŸ“Ļ Module System

🧩 What is a Module?

In Node.js, every JavaScript file is a module. A module is simply a reusable piece of code that you can import into other files. This keeps your code organized and maintainable.

📚

Three Types of Modules

👤
1. Your Own Modules
The JavaScript files you create for your project

Any .js file you write becomes a reusable module that can be imported into other files.

đŸ“Ļ
2. Built-in Node.js Modules
Modules that come bundled with Node.js

Powerful modules like fs, http, path, os ready to use — no installation needed!

🌐
3. Third-Party / External Modules
Community packages installed via npm

Thousands of packages like express, axios, lodash available to supercharge your projects!

🔄 Modular Programming: Two Ways

Node.js supports two module systems:

📜 CommonJS (Traditional)
require()
module.exports
Default in Node.js
✨ ES Modules (Modern)
import
export / export default
Supported in modern Node.js
📝 Note: Older Node.js versions required "type": "module" in package.json

📤 CommonJS: require() & module.exports

Step 1: Create a module file (math.js)

// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;

// Export functions so other files can use them
module.exports = {
  add,
  subtract,
  multiply
};

// Or export individually:
// module.exports.add = add;
// module.exports.subtract = subtract;
💡

ES6+ Object Shorthand: Notice { add, subtract, multiply } instead of { add: add, subtract: subtract, multiply: multiply }. When the property name and variable name are the same, you can use the shorthand! Use name: value only when you want to rename: { addition: add }.

Step 2: Import and use the module (app.js)

// app.js
const math = require('./math.js'); // ./ means "in the same directory"

console.log(math.add(5, 3));       // 8
console.log(math.subtract(10, 4)); // 6
console.log(math.multiply(3, 7));  // 21

// Or use destructuring:
const { add, multiply } = require('./math.js');
console.log(add(2, 2));      // 4
console.log(multiply(5, 5)); // 25
â–ļ 🔍 Learn More: How require() Works Under the Hood
âš™ī¸ What Happens When You Call require('./math.js')?
  1. Resolve the Path: Node.js figures out the full path to math.js
  2. Check Cache: Has this file been loaded before? If yes, return the cached result (skip to step 5)
  3. Load & Wrap: Read the file and wrap it in a function with special variables (module, exports, require, __dirname, __filename)
  4. Execute: Run the entire file from top to bottom
  5. Return: Return whatever is stored in module.exports
🚀 Module Caching: Execute Once, Use Many Times

Node.js caches modules after the first require(). This means the file is only executed once, even if you require it multiple times!

// counter.js
console.log('⚡ Counter module loaded!');

let count = 0;

module.exports = {
  increment: () => ++count,
  getCount: () => count
};
// app.js
const counter1 = require('./counter.js'); // Logs: "⚡ Counter module loaded!"
const counter2 = require('./counter.js'); // Nothing logged! (cached)
const counter3 = require('./counter.js'); // Nothing logged! (cached)

counter1.increment(); // count = 1
counter2.increment(); // count = 2 (same instance!)
counter3.increment(); // count = 3 (same instance!)

console.log(counter1.getCount()); // 3
console.log(counter2.getCount()); // 3
console.log(counter3.getCount()); // 3

// All three variables point to the SAME module instance!
💡

Key Insight: This caching behavior is a performance optimization. It prevents unnecessary file reads and re-execution, making your app faster!

đŸ“Ļ The Module Wrapper Function

Before executing your code, Node.js wraps it in a function. This is why you have access to special variables like module, exports, require, __dirname, and __filename!

// Your code in math.js:
const add = (a, b) => a + b;
module.exports = { add };

// What Node.js actually runs:
(function(exports, require, module, __filename, __dirname) {
  const add = (a, b) => a + b;
  module.exports = { add };
});
✨

This explains:

  • Why variables in one module don't pollute other modules (function scope!)
  • Where module, exports, and require come from
  • How you can access __dirname and __filename without importing them
đŸ—‚ī¸ Where Is The Cache Stored?

Node.js stores cached modules in require.cache. You can inspect or even clear it!

// See all cached modules
console.log(require.cache);

// Clear cache for a specific module (rarely needed!)
delete require.cache[require.resolve('./math.js')];

// Now the next require() will execute the file again
const math = require('./math.js');

✨ ES Modules: import & export

Step 1: Create a module file (math.js)

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;

// Or export default:
export default {
  add,
  subtract,
  multiply
};

Step 2: Import and use the module (app.js)

// app.js
import { add, multiply } from './math.js';

console.log(add(5, 3));      // 8
console.log(multiply(3, 7)); // 21

// Or import everything:
import * as math from './math.js';
console.log(math.add(2, 2)); // 4

// Or import default export:
import mathFunctions from './math.js';
console.log(mathFunctions.add(10, 5)); // 15
â–ļ 🔍 Learn More: How import/export Works Under the Hood
âš™ī¸ What Happens When You Use import?
  1. Static Analysis: Before execution, Node.js scans all import statements (they must be at the top!)
  2. Resolve Dependencies: Build a dependency graph of all modules
  3. Load Modules: Fetch all module files in parallel
  4. Parse & Link: Parse each module and link imports to exports
  5. Execute: Run modules in the correct order (dependencies first)
  6. Cache: Store the module instance (similar to CommonJS)
📍 Static Imports: Decided at Parse Time

ES Modules are static — all import and export statements are analyzed before the code runs. This enables powerful optimizations!

ī¸âœ… Valid: Imports must be at the top level
import { add } from './math.js';

// ❌ Invalid: Can't import conditionally (parse-time, not runtime!)
if (condition) {
  import { add } from './math.js'; // SyntaxError!
}

// ❌ Invalid: Can't import in functions
function doSomething() {
  import { add } from './math.js'; // SyntaxError!
}

// ✅ Valid: Use dynamic import() for conditional loading
if (condition) {
  const { add } = await import('./math.js'); // Works!
}
💡

Why Static? This allows tools to detect unused exports, eliminate dead code (tree shaking), and optimize bundles — impossible with dynamic require()!

🚀 ES Modules Are Cached Too!

Just like CommonJS, ES Modules are executed once and cached. Multiple imports get the same instance!

// counter.js
console.log('⚡ Counter module loaded!');

let count = 0;

export const increment = () => ++count;
export const getCount = () => count;
// app.js
import { increment as inc1, getCount as get1 } from './counter.js'; // Logs: "⚡"
import { increment as inc2, getCount as get2 } from './counter.js'; // Nothing!
import { increment as inc3, getCount as get3 } from './counter.js'; // Nothing!

inc1(); // count = 1
inc2(); // count = 2 (same instance!)
inc3(); // count = 3 (same instance!)

console.log(get1()); // 3
console.log(get2()); // 3
console.log(get3()); // 3
🔗 Live Bindings: ES Modules Magic!

Unlike CommonJS (which copies values), ES Module imports are live bindings — they reference the actual variable in the exporting module!

// counter.js
export let count = 0;

export function increment() {
  count++;
}

export function getCount() {
  return count;
}
// app.js
import { count, increment } from './counter.js';

console.log(count);  // 0

increment();
console.log(count);  // 1 (updated automatically! đŸĒ„)

increment();
console.log(count);  // 2 (live binding in action!)
âš ī¸

Important: You can READ the imported variable, but you cannot REASSIGN it. The exporting module owns the variable!

⚡ Dynamic Imports: Load on Demand

Need conditional or lazy loading? Use import() as a function — it returns a Promise!

// Load module conditionally
if (userWantsAdvancedFeatures) {
  const advanced = await import('./advanced-features.js');
  advanced.doSomething();
}

// Load multiple modules dynamically
const modules = await Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js')
]);

// Lazy load heavy libraries
button.addEventListener('click', async () => {
  const { chart } = await import('./heavy-chart-library.js');
  chart.render();
});
✨

Performance Win: Dynamic imports enable code splitting — load only what you need, when you need it!

âš–ī¸

CommonJS vs ES Modules

Understanding the differences

Feature CommonJS require() ES Modules import
Syntax const x = require('x') import x from 'x'
Loading Synchronous (blocking) Asynchronous (non-blocking)
When Resolved Runtime (dynamic) Parse time (static)
Conditional Imports ✅ Yes if(x) require() ❌ No (use import() instead)
Import Location ✅ Anywhere in code ❌ Top-level only
Bindings Copies values (snapshot) Live bindings (references)
Tree Shaking ❌ No (dynamic) ✅ Yes (static analysis)
Default in Node.js ✅ Yes ✅ Yes (modern versions)
Caching ✅ Yes (require.cache) ✅ Yes (module map)
Browser Support ❌ No (Node.js only) ✅ Yes (native support)
âš–ī¸ CommonJS vs ES Modules: Which Should You Use?

Both work great! Here's when to use each:

📜 Use CommonJS when:
  • Working with legacy Node.js projects
  • Need dynamic imports based on runtime conditions
  • Most npm packages still use it
✨ Use ES Modules when:
  • Starting new projects (modern standard)
  • Want better tooling support (tree shaking, bundlers)
  • Building libraries that work in both Node.js and browsers
  • Need live bindings or asynchronous loading
💡

The Future is ES Modules: They work in both Node.js AND browsers natively! CommonJS is Node.js-only. You can mix both systems in the same project (but not in the same file).

📘 Node.js Built-in Modules

Node.js comes with many powerful built-in modules that you can use without installing anything. Let's explore some essential ones!

đŸ’ģ os Module (Operating System)

Get information about your computer's operating system:

const os = require('os');

// System information
console.log('Platform:', os.platform());      // 'darwin', 'win32', 'linux'
console.log('Architecture:', os.arch());      // 'x64', 'arm64'
console.log('CPU Cores:', os.cpus().length);  // Number of CPU cores
console.log('Total Memory:', os.totalmem() / (1024**3), 'GB');
console.log('Free Memory:', os.freemem() / (1024**3), 'GB');
console.log('Home Directory:', os.homedir()); // User's home directory
console.log('Uptime:', os.uptime() / 3600, 'hours'); // System uptime

âŒ¨ī¸ readline/promises Module (User Input)

Read user input from the command line with simple async/await syntax:

const readline = require('readline/promises');

// Create interface for reading input
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

// Simple async function to get user info
async function getUserInfo() {
  const name = await rl.question('What is your name? ');
  const age = await rl.question('How old are you? ');
  
  console.log(`\nHello, ${name}! You are ${age} years old.`);
  
  rl.close();
}

getUserInfo();

📁 fs Module (File System)

The fs module lets you work with files and directories. It comes in three flavors:

🔀 Three Versions of fs Module

đŸšĢ
fs (Synchronous) - ❌ AVOID
Blocks your program until operation completes
const data = fs.readFileSync('file.txt', 'utf8');
âš ī¸ Freezes your app — don't use in production!
âš ī¸
fs (Async with Callbacks) - 😕 OLD STYLE
Non-blocking but uses callbacks (callback hell)
fs.readFile('file.txt', 'utf8', (err, data) => { ... });
Better, but callbacks can get messy
✅
fs/promises (Promises) - ✨ USE THIS!
Non-blocking + clean async/await syntax
const data = await fs.promises.readFile('file.txt', 'utf8');
✨ Modern, clean, and the recommended way!
đŸŽ¯

Best Practice: Always use fs/promises with async/await. It's non-blocking, modern, and much easier to read than callbacks!

💡 Two Ways to Import fs/promises

Option 1: Import the promises submodule directly (recommended)

const fs = require('fs/promises');

Option 2: Import fs and access promises property

const fs = require('fs').promises;

Both work the same way! We'll use Option 1 in our examples.

📖 Reading Files (with fs/promises)

const fs = require('fs/promises');

async function readFile() {
  try {
    // Read entire file as string
    const data = await fs.readFile('example.txt', 'utf8');
    console.log('File contents:');
    console.log(data);
  } catch (error) {
    console.error('Error reading file:', error.message);
  }
}

readFile();

âœī¸ Writing Files (with fs/promises)

const fs = require('fs/promises');

async function writeFile() {
  try {
    const content = 'Hello from Node.js!\nThis is a new file.';
    
    // Write to file (creates file if it doesn't exist, overwrites if it does)
    await fs.writeFile('output.txt', content, 'utf8');
    console.log('File written successfully!');
    
    // Append to file (adds to the end)
    await fs.appendFile('output.txt', '\nAppended line!', 'utf8');
    console.log('Content appended!');
  } catch (error) {
    console.error('Error writing file:', error.message);
  }
}

writeFile();

📂 Working with Directories

const fs = require('fs/promises');

async function workWithDirectories() {
  try {
    // Create a directory
    await fs.mkdir('myFolder', { recursive: true }); // recursive: true won't error if exists
    console.log('Directory created!');
    
    // List files in a directory
    const files = await fs.readdir('.');
    console.log('Files in current directory:', files);
    
    // Check if file/directory exists
    try {
      await fs.access('myFolder');
      console.log('myFolder exists!');
    } catch {
      console.log('myFolder does not exist');
    }
    
    // Delete a file
    await fs.unlink('fileToDelete.txt');
    
    // Delete an empty directory
    await fs.rmdir('myFolder');
    
  } catch (error) {
    console.error('Error:', error.message);
  }
}

workWithDirectories();

đŸ”Ĩ Complete File System Example

const fs = require('fs/promises');
const os = require('os');

async function fileSystemDemo() {
  try {
    console.log('=== File System Demo ===\n');
    
    // 1. Write data to a file
    const userData = {
      name: 'John Doe',
      age: 30,
      platform: os.platform()
    };
    
    await fs.writeFile('user.json', JSON.stringify(userData, null, 2), 'utf8');
    console.log('✓ Created user.json');
    
    // 2. Read the file back
    const fileContent = await fs.readFile('user.json', 'utf8');
    const parsedData = JSON.parse(fileContent);
    console.log('✓ Read user data:', parsedData);
    
    // 3. Create a logs directory
    await fs.mkdir('logs', { recursive: true });
    console.log('✓ Created logs directory');
    
    // 4. Write a log file
    const logMessage = `[${new Date().toISOString()}] User data accessed\n`;
    await fs.appendFile('logs/app.log', logMessage, 'utf8');
    console.log('✓ Logged activity');
    
    // 5. List all files in current directory
    const files = await fs.readdir('.');
    console.log('✓ Files in current directory:', files);
    
  } catch (error) {
    console.error('❌ Error:', error.message);
  }
}

fileSystemDemo();

🌐 Building HTTP Servers

🌐

Web Development with Node.js

Now that we understand Node.js fundamentals, let's complete our web development journey by building HTTP servers! This is where Node.js really shines — creating backend services, REST APIs, and web applications.

📡 The http Module

Node.js comes with a built-in http module that allows you to create web servers. A server listens for incoming HTTP requests and sends back responses. This is the foundation of all web applications!

đŸ—ī¸ Creating Your First Server

Let's build a simple HTTP server step by step:

const http = require('http');

// Create a server
const server = http.createServer((req, res) => {
  // This callback runs for EVERY incoming request
  console.log('New request received!');
  
  // Set response headers
  res.statusCode = 200; // 200 = OK
  res.setHeader('Content-Type', 'text/plain');
  
  // Send response
  res.end('Hello from Node.js server!');
});

// Start listening on port 3000
const PORT = 3000;
server.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}/`);
});

// Now open your browser and go to: http://localhost:3000
✅ Success!

Run this file with node server.js, then visit http://localhost:3000 in your browser. You've just created a web server! 🎉

🔍 Understanding the Server Components

🧩 Server Components Breakdown

âš™ī¸
http.createServer(callback)
Creates a server instance
const server = http.createServer((req, res) => { ... });
The callback runs for every request
👂
server.listen(port, callback)
Starts the server on a specific port
server.listen(3000, () => console.log('Server started'));
Port 3000 is a common choice for development
đŸ“Ĩ
req (Request Object)
Contains information about the incoming request
â€ĸ req.url - requested path
â€ĸ req.method - HTTP method (GET, POST, etc.)
â€ĸ req.headers - request headers
â–ļ 🔍 Deep Dive: Request Object Properties & Methods
🔗 URL & Path Information
Property Description
req.url Full path including query string
'/users?id=123&name=John'
req.httpVersion HTTP version used
'1.1' or '2.0'
🔨 HTTP Method
Property Description
req.method HTTP verb used for the request
'GET', 'POST', 'PUT', 'DELETE', 'PATCH'
📋 Headers
Property Description
req.headers Object containing all request headers (lowercase keys)
req.headers['content-type']
req.headers['user-agent'] Browser/client information
req.headers['content-type'] Format of request body
'application/json', 'text/html'
req.headers['authorization'] Authentication credentials
req.headers['cookie'] Browser cookies sent with request
🔌 Connection Information
Property Description
req.socket Underlying network socket
req.socket.remoteAddress Client's IP address
'::1' (localhost) or '192.168.1.100'
req.socket.remotePort Client's port number
📡 Stream Methods (for reading body)
Event/Method Description
req.on('data', fn) Fires when a chunk of body data arrives
req.on('end', fn) Fires when all body data has been received
req.on('error', fn) Fires if an error occurs while reading
💡 Complete Example
const server = http.createServer((req, res) => {
  console.log('Method:', req.method);
  console.log('URL:', req.url);
  console.log('HTTP Version:', req.httpVersion);
  console.log('User Agent:', req.headers['user-agent']);
  console.log('Client IP:', req.socket.remoteAddress);
  
  res.end('Request logged!');
});

🧩 Server Components Breakdown

📤
res (Response Object)
Used to send data back to the client
â€ĸ res.statusCode - HTTP status code
â€ĸ res.setHeader() - set response headers
â€ĸ res.end() - send response and close
â–ļ 🔍 Deep Dive: Response Object Properties & Methods
📊 Status Code
Property Description
res.statusCode HTTP status code to send
200 (OK), 404 (Not Found), 500 (Server Error)
Default: 200
res.statusMessage Status message text
'OK', 'Not Found', 'Internal Server Error'

Common Status Codes:

â€ĸ 2xx Success: 200 (OK), 201 (Created), 204 (No Content)
â€ĸ 3xx Redirect: 301 (Moved Permanently), 302 (Found), 304 (Not Modified)
â€ĸ 4xx Client Error: 400 (Bad Request), 401 (Unauthorized), 404 (Not Found)
â€ĸ 5xx Server Error: 500 (Internal Server Error), 503 (Service Unavailable)
📋 Header Methods
Method Description
res.setHeader(name, value) Set a single header
res.setHeader('Content-Type', 'application/json')
res.getHeader(name) Get a previously set header value
res.removeHeader(name) Remove a header before sending response
res.hasHeader(name) Check if header exists (returns boolean)
res.writeHead(status, headers) Set status code and multiple headers at once
res.writeHead(200, {'Content-Type': 'text/html'})

Common Response Headers:

â€ĸ Content-Type: 'text/html', 'application/json', 'text/plain'
â€ĸ Content-Length: Size of response body in bytes
â€ĸ Set-Cookie: Send cookies to browser
â€ĸ Location: URL for redirects
â€ĸ Cache-Control: Caching behavior
â€ĸ Access-Control-Allow-Origin: CORS policy
âœī¸ Writing Response Body
Method Description
res.write(data) Write a chunk of response body (can be called multiple times)
res.write('Hello '); res.write('World');
res.end([data]) Finish response and optionally send final data
res.end('Goodbye!');
âš ī¸ Must be called on every response!
âš ī¸
Critical Timing: When you call res.write() for the first time, Node.js immediately sends the HTTP headers to the client, then sends your data. After that, you cannot modify headers! Same with res.end() — it sends headers first (if not sent yet), then the final data.

Order: Set all headers → res.write() / res.end() → Headers sent to client → Body sent to client
🔍 Response State
Property Description
res.headersSent Boolean - true if headers have been sent
if (!res.headersSent) res.setHeader(...)
res.finished Boolean - true if response has been completed (res.end() called)
res.writableEnded Boolean - true if res.end() has been called
💡 Complete Example
const server = http.createServer((req, res) => {
  // Set status code
  res.statusCode = 200;
  
  // Set multiple headers
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('X-Custom-Header', 'MyValue');
  
  // Alternative: use writeHead
  // res.writeHead(200, {
  //   'Content-Type': 'application/json',
  //   'X-Custom-Header': 'MyValue'
  // });
  
  // Write body in chunks (optional)
  res.write('{"message": "');
  res.write('Hello');
  res.write('"}');
  
  // Finish response
  res.end();
  
  // Or do it all in one:
  // res.end(JSON.stringify({ message: 'Hello' }));
});

đŸ“Ļ Reading Request Body (POST Data)

Reading the request body is more complex because Node.js streams data in chunks. This is efficient for large data but requires careful handling:

âš ī¸ Important: Data Comes in Chunks!

The request body doesn't arrive all at once. It comes in chunks (small pieces). You must listen for these chunks and combine them. This is a fundamental concept in Node.js streams!

💡 Two Ways to Collect Chunks

Simple Approach: String concatenation (works great for text/JSON)

let body = '';
req.on('data', chunk => body += chunk.toString());

Buffer Approach: For binary data or more control

let chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => Buffer.concat(chunks).toString());

We'll use the simple string approach below — it's easier and works perfectly for JSON/text data!

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.method === 'POST') {
    let body = '';
    
    // Listen for 'data' event - fires when a chunk arrives
    req.on('data', (chunk) => {
      body += chunk.toString(); // Simply concatenate strings!
    });
    
    // Listen for 'end' event - fires when all chunks received
    req.on('end', () => {
      console.log('Complete body:', body);
      
      // Parse JSON if needed
      try {
        const data = JSON.parse(body);
        console.log('Parsed data:', data);
        
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({ 
          message: 'Data received!', 
          received: data 
        }));
      } catch (error) {
        res.statusCode = 400;
        res.end('Invalid JSON');
      }
    });
    
  } else {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Send a POST request with JSON data');
  }
});

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});
đŸ§Ē Test with cURL or Postman
# Test with cURL
curl -X POST http://localhost:3000 \
  -H "Content-Type: application/json" \
  -d '{"name":"John","age":30}'

🔄 Understanding the Chunk Process

📊 Why Chunks? Performance & Memory!

1
Efficient Memory Usage: Instead of loading a huge file into memory at once, Node.js processes it piece by piece
2
Non-blocking: Server can handle other requests while waiting for data chunks
3
Scalability: Can handle file uploads and large payloads without crashing
How it works:
1ī¸âƒŖ Server reads chunk from file
2ī¸âƒŖ Sends chunk over network
3ī¸âƒŖ Client receives & processes chunk
4ī¸âƒŖ Only then does server send next chunk
✓ Sequential = Memory efficient & reliable

📤 Sending Responses

The res object is used to send data back to the client:

const http = require('http');

const server = http.createServer((req, res) => {
  // 1. Set status code
  res.statusCode = 200; // 200 = OK, 404 = Not Found, 500 = Server Error
  
  // 2. Set headers (metadata about the response)
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('X-Custom-Header', 'My Value');
  
  // 3. Send the response body and close connection
  const data = {
    message: 'Hello!',
    timestamp: new Date().toISOString()
  };
  res.end(JSON.stringify(data));
  
  // Note: res.end() MUST be called to complete the response!
});

server.listen(3000);

đŸ›Ŗī¸ Manual Routing with if/else

Before frameworks like Express (third-party module), routing was done manually with if/else statements. This is important to understand because it shows how routing really works:

const http = require('http');

const server = http.createServer((req, res) => {
  const { method, url } = req;
  
  // Home page
  if (url === '/' && method === 'GET') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html');
    res.end('

Welcome to my server!

About

'); } // About page else if (url === '/about' && method === 'GET') { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end('

About

This is a Node.js server

'); } // API endpoint - Get users else if (url === '/api/users' && method === 'GET') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]; res.end(JSON.stringify(users)); } // API endpoint - Create user else if (url === '/api/users' && method === 'POST') { let body = []; req.on('data', (chunk) => { body.push(chunk); }); req.on('end', () => { const userData = JSON.parse(Buffer.concat(body).toString()); res.statusCode = 201; // 201 = Created res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ message: 'User created!', user: userData })); }); } // 404 - Not Found else { res.statusCode = 404; res.setHeader('Content-Type', 'text/html'); res.end('

404 - Page Not Found

'); } }); server.listen(3000, () => { console.log('Server running at http://localhost:3000'); console.log('Try:'); console.log(' - http://localhost:3000/'); console.log(' - http://localhost:3000/about'); console.log(' - http://localhost:3000/api/users'); });
💡 This Gets Messy Fast!

As your application grows, manual routing with if/else becomes difficult to maintain. That's why frameworks like Express exist — to simplify routing, middleware, and request handling. We'll cover Express next!

đŸŽ¯ Complete Server Example

Here's a complete server with all concepts combined:

const http = require('http');

// In-memory data store (normally you'd use a database)
const todos = [
  { id: 1, task: 'Learn Node.js', completed: false },
  { id: 2, task: 'Build a server', completed: true }
];

const server = http.createServer((req, res) => {
  const { method, url } = req;
  
  // Set CORS headers (allow requests from browsers)
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  
  // GET /api/todos - List all todos
  if (url === '/api/todos' && method === 'GET') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ todos }));
  }
  
  // POST /api/todos - Create a new todo
  else if (url === '/api/todos' && method === 'POST') {
    let body = [];
    
    req.on('data', (chunk) => body.push(chunk));
    
    req.on('end', () => {
      try {
        const data = JSON.parse(Buffer.concat(body).toString());
        const newTodo = {
          id: todos.length + 1,
          task: data.task,
          completed: false
        };
        todos.push(newTodo);
        
        res.statusCode = 201;
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({ message: 'Created!', todo: newTodo }));
      } catch (error) {
        res.statusCode = 400;
        res.end(JSON.stringify({ error: 'Invalid JSON' }));
      }
    });
  }
  
  // DELETE /api/todos/:id - Delete a todo
  else if (url.startsWith('/api/todos/') && method === 'DELETE') {
    const id = parseInt(url.split('/')[3]);
    const index = todos.findIndex(t => t.id === id);
    
    if (index !== -1) {
      todos.splice(index, 1);
      res.statusCode = 200;
      res.end(JSON.stringify({ message: 'Deleted!' }));
    } else {
      res.statusCode = 404;
      res.end(JSON.stringify({ error: 'Todo not found' }));
    }
  }
  
  // 404
  else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ error: 'Endpoint not found' }));
  }
});

const PORT = 3000;
server.listen(PORT, () => {
  console.log(`🚀 Todo API running at http://localhost:${PORT}`);
  console.log('Endpoints:');
  console.log('  GET    /api/todos      - List all todos');
  console.log('  POST   /api/todos      - Create a todo');
  console.log('  DELETE /api/todos/:id  - Delete a todo');
});
✅

You Now Understand HTTP Servers!

You've learned how servers work at a fundamental level: creating servers, handling requests, reading bodies in chunks, sending responses, and manual routing. This knowledge is essential, even when using frameworks!

đŸŽ¯

Challenge: Build Your Own Static File Server

For curious students who want to go deeper!

🤔 The Problem

Ever used tools like Live Server or http-server to serve HTML files locally? Let's build a simplified version! Your server should:

  • 📁 Serve HTML, CSS, JS, images from a folder
  • 🔗 Handle subfolders (e.g., /styles/main.css)
  • 📄 Send correct Content-Type headers
  • ❌ Show 404 for missing files
đŸ› ī¸ Tools You'll Need

You already know everything needed:

  • http module - Create the server
  • fs/promises module - Read files from disk
  • path module - Handle file paths safely
âš ī¸ The Naive Approach (Don't Do This!)

You might think: "I'll just use if/else for each file!"

// ❌ This gets messy FAST!
if (url === '/index.html') { /* read index.html */ }
else if (url === '/about.html') { /* read about.html */ }
else if (url === '/styles/main.css') { /* read main.css */ }
else if (url === '/js/script.js') { /* read script.js */ }
// ... hundreds of files? 😱
💡 The Smart Approach

Instead of hardcoding every file, dynamically map URLs to file paths:

  1. Get the requested URL (e.g., /styles/main.css)
  2. Map it to a file path (e.g., ./public/styles/main.css)
  3. Use fs.readFile() to read it
  4. Detect file type and set correct Content-Type
  5. Send file contents back to client

Bonus: Node.js's fs module has functions to traverse folders recursively — no need to reinvent the wheel!

â–ļ 💡 Reveal Complete Solution

Here's a complete static file server implementation:

const http = require('http');
const fs = require('fs/promises'); // Using fs/promises directly

// Public folder
const PUBLIC_FOLDER = 'public';

const server = http.createServer(async (req, res) => {
    // Clean the URL (remove query strings)
    let filePath = req.url.split('?')[0];
    
    // Default to index.html if requesting root
    if (filePath === '/') 
        filePath = '/index.html';
    
    // Build full path
    let fullPath = PUBLIC_FOLDER + filePath;

    // Try to read and serve the file
    try {
        const content = await fs.readFile(fullPath);
        
        // Send response
        res.statusCode = 200;
        res.end(content);
    }
    // If file doesn't exist or can't be read
    catch (e) {
        console.log(e)
        res.statusCode = 404
        res.end("not found")
    }
});

const PORT = 3000;
server.listen(PORT, () => {
    console.log(`📁 Static file server running at http://localhost:${PORT}/`);
});
📝 How to Use
  1. Create a folder called public/ in your project
  2. Add HTML, CSS, JS files to it (and subfolders)
  3. Run the server: node server.js
  4. Visit http://localhost:3000
🔍 Key Concepts Used
  • path.join() - Safely combine paths (handles OS differences)
  • path.extname() - Get file extension (e.g., .css)
  • fs.readFile() - Read entire file into memory
  • async/await - Clean error handling
  • Security check - Prevent ../../etc/passwd attacks!
  • MIME types - Browser knows how to handle each file
🎓
What You Learned: You just built a mini version of tools like Live Server! This demonstrates how web servers work under the hood. In production, you'd use battle-tested libraries like express.static() or serve-static, but now you understand what they do internally!

đŸ“Ļ npm & External Modules

So far, we've used Node.js built-in modules (like http, fs, os). But one of Node.js's superpowers is npm — the world's largest software registry with over 2 million packages! These are third-party/external modules created by developers worldwide.

đŸ“Ļ

What is npm?

npm (Node Package Manager) is both a command-line tool and an online registry. It lets you:

  • đŸ“Ĩ Install packages created by others
  • 🔄 Manage project dependencies
  • 📤 Publish your own packages
  • 🔐 Version control for dependencies

đŸŽŦ Initializing a Project: npm init

Before installing packages, you need to initialize your project. This creates a package.json file — the heart of every Node.js project.

# Initialize a new project (interactive)
npm init

# Quick init with defaults
npm init -y

# This creates package.json with your project metadata

📄 Understanding package.json

package.json is a JSON file that contains metadata about your project and lists all dependencies:

{
  "name": "my-node-project",
  "version": "1.0.0",
  "description": "My awesome Node.js project",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["nodejs", "api"],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}
🔑

Champs ClÊs dans package.json

📝 name

Nom du projet (requis pour la publication)

đŸˇī¸ version

Version actuelle (suit le versioning sÊmantique)

⚡ scripts

Commandes que vous pouvez exÊcuter avec npm run

đŸ“Ļ dependencies

Packages nÊcessaires en production

đŸ› ī¸ devDependencies

Packages nÊcessaires uniquement pour le dÊveloppement

đŸ“Ĩ Installing Packages: npm install

# Install a package and add to dependencies
npm install express
# or shorthand:
npm i express

# Install multiple packages
npm install express mongoose dotenv

# Install as a dev dependency (only for development)
npm install --save-dev nodemon
# or shorthand:
npm i -D nodemon

# Install all dependencies from package.json
npm install

# Install a specific version
npm install express@4.17.1

# Install globally (available everywhere on your system)
npm install -g nodemon
â„šī¸ dependencies vs devDependencies

dependencies: Packages your app needs to run (Express, database drivers, etc.)

devDependencies: Packages only needed during development (testing tools, build tools, nodemon, etc.)

đŸ“Ļ What Happens When You Install?

Three things are created/updated:

📄
package.json

Updated with the package name and version in dependencies or devDependencies

"express": "^4.18.2"
🔒
package-lock.json

Locks the exact versions of all packages and their dependencies. Ensures everyone gets the same versions.

✓ Commit this to git!
📁
node_modules/

Folder containing the actual code of all installed packages and their dependencies. Can be huge!

✗ Never commit to git!
Add to .gitignore
âš ī¸ Important: .gitignore

Never commit node_modules/ to git! It's huge and can be regenerated with npm install.

# .gitignore
node_modules/
.env

đŸŽ¯ npm Scripts

The scripts field in package.json lets you define custom commands. This is incredibly useful for common tasks!

// package.json
{
  "scripts": {
    "start": "node server.js",
    "test": "jest",
    "build": "webpack"
  }
}

Run scripts with npm run:

# Special script - can omit "run"
npm start
# Same as: node server.js

# Other scripts require "run"
npm run test
# Same as: jest

npm run build
# Same as: webpack
💡 Why Configure npm start?

npm start is a standard convention. When others clone your project, they can immediately run npm start without needing to know your file structure. It also works on platforms like Heroku and Vercel that look for this script!

🔍 Useful npm Commands

📋
List Installed Packages
npm list
npm list --depth=0

Shows all installed packages. Use --depth=0 to show only top-level packages (without dependencies).

🔍
Check Outdated Packages
npm outdated

Lists packages that have newer versions available. Shows current version, wanted version (respects semver), and latest version.

âŦ†ī¸
Update Packages
npm update
npm update express

Updates packages to the latest version allowed by your package.json version range. Without arguments, updates all packages.

đŸ—‘ī¸
Uninstall Packages
npm uninstall express
npm uninstall -D nodemon

Removes package from node_modules/ and package.json. Use -D for devDependencies or it auto-detects.

â„šī¸
View Package Info
npm view express
npm info express

Displays detailed information about a package from the npm registry: versions, dependencies, repository, maintainers, etc.

🔎
Search Packages
npm search mongodb
npm search testing

Searches the npm registry for packages matching keywords. Returns package names, descriptions, and authors.

🧹
Clean Cache
npm cache clean --force
npm cache verify

Clears npm's cache. Use when having installation issues. verify checks cache integrity without deleting.

📌
Install Specific Version
npm install express@4.18.0
npm install lodash@latest

Installs a specific version of a package. Use @latest for newest version, or specify exact version number.

📚 Popular npm Packages You Should Know

🌐 express
Web framework for building APIs
đŸ—„ī¸ mongoose
MongoDB object modeling
🔐 dotenv
Load environment variables
📡 axios
HTTP client for making requests
🔄 nodemon
Auto-restart on file changes
🔍 lodash
Utility functions for JS

nodemon

😤 The Problem

Every time you change your code, you have to manually stop the server (Ctrl+C) and restart it. This gets annoying fast during development!

✨ The Solution: nodemon

nodemon automatically restarts your Node.js application when it detects file changes. It's a must-have for development!

đŸ“Ĩ Installation

Install nodemon as a dev dependency in your project (you don't need it in production):

# Install as dev dependency
npm install --save-dev nodemon

# or shorthand:
npm i -D nodemon

🚀 Usage with npm Scripts

Since we learned about npm scripts earlier, let's add a dev script to package.json:

// package.json
{
  "scripts": {
    "start": "node server.js",      // For production
    "dev": "nodemon server.js"      // For development with auto-restart
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

Now run your development server with:

# Start with auto-restart
npm run dev

# Output:
# [nodemon] 3.0.1
# [nodemon] to restart at any time, enter `rs`
# [nodemon] watching path(s): *.*
# [nodemon] watching extensions: js,mjs,json
# [nodemon] starting `node server.js`
# Server running on http://localhost:3000

Now when you save changes to your code, nodemon automatically restarts the server! 🎉

⚡ Visual Comparison

❌ Without nodemon
âœī¸ 1. Edit code
💾 2. Save file
âšī¸ 3. Press Ctrl+C to stop server
â–ļī¸ 4. Run node server.js again
🔄 5. Repeat for every change...
✅ With nodemon
âœī¸ 1. Edit code
💾 2. Save file
✨ 3. Auto-restart! 🎉
That's it! 🚀

âš™ī¸ npm Scripts Configuration

As you can see, using npm scripts makes it easy to run nodemon. You don't need to remember the exact commands - just use npm run dev!

💡 Pro Tip

Type rs in the terminal and press Enter to manually restart nodemon anytime, even without file changes!

đŸ› ī¸ Advanced Configuration: nodemon.json

Create a nodemon.json file in your project root for advanced settings:

// nodemon.json
{
  "watch": ["src"],                    // Only watch specific folders
  "ext": "js,json,html",               // Watch specific file types
  "ignore": ["test/*", "docs/*"],      // Ignore certain files/folders
  "delay": "1000",                     // Wait 1 second before restarting
  "env": {
    "NODE_ENV": "development",         // Set environment variables
    "PORT": "3000"
  }
}
⚡

Development Workflow Supercharged!

With nodemon, you can focus on coding instead of constantly restarting your server.
It's a game-changer for development productivity! 🚀

🔐 Environment Variables

🤔 What Are Environment Variables?

Environment variables are key-value pairs stored in your operating system that your application can read at runtime. They're essential for storing configuration and secrets that change between environments (development, staging, production).

âš ī¸ Why Use Environment Variables?

🔒
Security

Never hardcode secrets! API keys, database passwords, and tokens should never be in your source code. If you push them to Git, they're exposed forever!

🔄
Flexibility

Use different configurations for different environments: development database locally, production database on server — same code!

đŸ‘Ĩ
Team Collaboration

Each developer can have their own local configuration without conflicts. Your teammate's database password stays private.

🔑 Accessing Environment Variables: process.env

Node.js provides the process.env object to access environment variables:

// Access environment variables
console.log(process.env.NODE_ENV);      // 'development', 'production', etc.
console.log(process.env.PORT);          // '3000'
console.log(process.env.DATABASE_URL);  // Database connection string
console.log(process.env.API_KEY);       // Secret API key

// Provide default values
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';

// Use in your code
const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT} in ${NODE_ENV} mode`);
});

âš™ī¸ Setting Environment Variables

đŸ’ģ
1. Command Line (Temporary)

Set variables when running your app:

# Linux/Mac
PORT=3000 NODE_ENV=production node app.js

# Windows (Command Prompt)
set PORT=3000 && node app.js

# Windows (PowerShell)
$env:PORT=3000; node app.js
âš ī¸ These only last for the current terminal session!
đŸ“Ļ
3. npm Scripts

Set in package.json scripts:

// package.json
{
  "scripts": {
    "start": "NODE_ENV=production node server.js",
    "dev": "NODE_ENV=development nodemon server.js"
  }
}

đŸ“Ļ Using the dotenv Package

The dotenv package automatically loads variables from your .env file into process.env. It's the industry standard!

1ī¸âƒŖ Install dotenv:
npm install dotenv
2ī¸âƒŖ Create a .env file:
# .env
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=abc123xyz456
NODE_ENV=development
3ī¸âƒŖ Load in your application:
// Load dotenv at the very top of your main file
require('dotenv').config();

// Now you can access environment variables
const PORT = process.env.PORT || 3000;
const DATABASE_URL = process.env.DATABASE_URL;
const API_KEY = process.env.API_KEY;

console.log('Starting server on port:', PORT);
console.log('Database:', DATABASE_URL);
// Never log API keys in production!
💡 Complete Example
// server.js
require('dotenv').config(); // Load .env file first!

const express = require('express');
const app = express();

// Use environment variables
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';
const DATABASE_URL = process.env.DATABASE_URL;
const API_KEY = process.env.API_KEY;

// Example: Different behavior per environment
if (NODE_ENV === 'development') {
  // Detailed logging in development
  app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
  });
}

// Use API_KEY for external services
app.get('/weather', async (req, res) => {
  const response = await fetch(
    `https://api.weather.com/data?key=${API_KEY}`
  );
  const data = await response.json();
  res.json(data);
});

app.listen(PORT, () => {
  console.log(`✅ Server running on port ${PORT}`);
  console.log(`📊 Environment: ${NODE_ENV}`);
  console.log(`đŸ—„ī¸  Database: ${DATABASE_URL}`);
});
🔒

Critical Security: .gitignore

NEVER commit .env to Git! Add it to .gitignore immediately:

# .gitignore
.env
.env.local
.env.*.local
node_modules/

Instead: Create a .env.example file with placeholder values (safe to commit):

# .env.example (safe to commit)
PORT=3000
DATABASE_URL=your_database_url_here
API_KEY=your_api_key_here
NODE_ENV=development

Your teammates copy .env.example to .env and fill in their own values!

📋 Common Environment Variables

NODE_ENV

'development', 'production', 'test' — controls app behavior

PORT

Port number for your server (e.g., 3000, 8080)

DATABASE_URL

Connection string for your database

API_KEY

Secret keys for third-party APIs

JWT_SECRET

Secret key for signing JSON Web Tokens

CORS_ORIGIN

Allowed origins for Cross-Origin requests

✨ Best Practices

  • Always use .env for local development — don't set environment variables manually
  • Never commit .env to version control — use .gitignore
  • Provide .env.example — document required variables for teammates
  • Provide defaults — use process.env.PORT || 3000 as fallback
  • Validate required variables — check critical vars exist on startup
  • Different values per environment — dev database locally, prod database on server
  • Load dotenv early — first line in your entry file
🚀

Production Deployment

On production servers (Heroku, AWS, Vercel, etc.), you don't use .env files. Instead, you set environment variables through the platform's dashboard or CLI. This keeps secrets secure and separate from your code!

Express Framework

đŸŽ¯ Why Express?

Building servers with the raw http module requires a lot of boilerplate code. Express is a minimal, fast, and flexible Node.js framework that makes building web servers incredibly simple.

đŸ›¤ī¸ Simple routing: Define routes with clean syntax
🔧 Middleware: Add functionality like body parsing, CORS, authentication
đŸ“Ļ Request parsing: Automatic JSON/URL-encoded body parsing

đŸ“Ĩ Installation

# Install Express
npm install express

# Your package.json will have:
# "dependencies": {
#   "express": "^4.18.2"
# }

🚀 Your First Express Server

Compare how much simpler Express is compared to the raw http module:

❌ With http module (verbose)
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World');
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});
✅ With Express (clean & simple)
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
✨ Much Cleaner!

Express handles status codes, headers, and routing automatically. You focus on the logic, not the boilerplate!

đŸ›¤ī¸ Routing

Express makes defining routes incredibly simple. Each HTTP method has its own function:

const express = require('express');
const app = express();

// GET request
app.get('/users', (req, res) => {
  res.send('Get all users');
});

// POST request
app.post('/users', (req, res) => {
  res.send('Create a user');
});

// PUT request
app.put('/users/:id', (req, res) => {
  res.send(`Update user ${req.params.id}`);
});

// DELETE request
app.delete('/users/:id', (req, res) => {
  res.send(`Delete user ${req.params.id}`);
});

app.listen(3000);

📌 Route Parameters

Extract dynamic values from the URL using :paramName:

// Route with parameters
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`User ID: ${userId}`);
});

// Multiple parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.json({ userId, postId });
});

// Example: GET /users/123/posts/456
// Response: { "userId": "123", "postId": "456" }

🔍 Query Parameters

Access query strings with req.query:

app.get('/search', (req, res) => {
  const { q, page, limit } = req.query;
  res.json({ 
    query: q, 
    page: page || 1, 
    limit: limit || 10 
  });
});

// Example: GET /search?q=nodejs&page=2&limit=20
// Response: { "query": "nodejs", "page": "2", "limit": "20" }

🔧 Middleware

Middleware functions are executed before your route handlers. They can modify the request/response or terminate the request-response cycle.

âš™ī¸ Middleware Pipeline

đŸ“Ĩ
Request
1ī¸âƒŖ
Middleware 1
Logging
2ī¸âƒŖ
Middleware 2
Body Parsing
đŸŽ¯
Route Handler
Your Code
📤
Response
const express = require('express');
const app = express();

// Custom logging middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next(); // Pass control to the next middleware/route
});

// Custom auth middleware (example)
const checkAuth = (req, res, next) => {
  const token = req.headers['authorization'];
  if (token === 'secret-token') {
    next(); // Authorized, continue
  } else {
    res.status(401).json({ error: 'Unauthorized' });
  }
};

// Apply middleware to specific route
app.get('/protected', checkAuth, (req, res) => {
  res.send('This is protected!');
});

app.listen(3000);

đŸ“Ļ Body Parsing with express.json()

To read JSON data from POST/PUT requests, use the built-in express.json() middleware:

const express = require('express');
const app = express();

// Enable JSON body parsing
app.use(express.json());

app.post('/users', (req, res) => {
  const userData = req.body; // Access the JSON body
  console.log('Received:', userData);
  
  res.status(201).json({
    message: 'User created',
    user: userData
  });
});

// Example request:
// POST /users
// Body: { "name": "Alice", "email": "alice@example.com" }
// Response: { "message": "User created", "user": { "name": "Alice", ... } }
💡 Remember the http module?

With raw http, you had to manually listen to data and end events, concatenate chunks, and parse JSON. Express does all of that for you with express.json()! 🎉

📝 Parsing Text with express.text()

If you need to read plain text (not JSON) from the request body, use express.text():

const express = require('express');
const app = express();

// Parse text/plain bodies
app.use(express.text());

app.post('/message', (req, res) => {
  console.log('Received text:', req.body);
  // req.body is a string (not an object!)
  
  res.send(`You sent: ${req.body}`);
});

// Example request:
// POST /message
// Content-Type: text/plain
// Body: "Hello, this is plain text!"
// Response: "You sent: Hello, this is plain text!"

app.listen(3000);
âš ī¸ express.text() vs express.json()

express.json() → req.body is an object (parsed JSON)
express.text() → req.body is a string (plain text)
express.urlencoded() → req.body is an object (form data)

🔗 Understanding Middleware: The Request Pipeline

Middleware is one of Express's most powerful features. Think of it as a queue of filters that every request passes through before reaching your route handler.

đŸŽ¯ What is Middleware?

Middleware are functions that have access to the request (req), response (res), and the next function. Each middleware can:

  • ✅ Execute any code
  • ✅ Modify req and res objects
  • ✅ End the request-response cycle
  • ✅ Call next() to pass control to the next middleware
🔄 Request Flow: The Middleware Queue
đŸ“Ĩ
Incoming Request
Client sends HTTP request
↓
🔧
Middleware 1
Logger → calls next()
↓
🔧
Middleware 2
JSON Parser → calls next()
↓
🔧
Middleware 3
Auth Check → calls next()
↓
đŸŽ¯
Route Handler
Your actual route logic
↓
📤
Send Response
res.json(), res.send(), etc.
⚡ The next() Function: Critical!

You MUST call next() to pass control to the next middleware in the queue. If you forget, the request will hang forever — the client never gets a response!

✅ Correct: Calls next()
app.use((req, res, next) => {
  console.log('Request received');
  next(); // ✅ Passes control!
});
❌ Wrong: Forgets next()
app.use((req, res, next) => {
  console.log('Request received');
  // ❌ No next()! Request hangs!
});
Exception: If you send a response (res.send(), res.json()), you don't need next() because the request-response cycle ends there.
đŸ› ī¸ Middleware Examples

Middleware functions are the building blocks of Express applications. They execute in sequence, allowing you to process requests, modify responses, and control the flow of your application.

📝
1. Logger Middleware
Application-Level

Logs every request that comes to your server:

const express = require('express');
const app = express();

// Custom logger middleware
app.use((req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${req.method} ${req.url}`);
  next(); // ✅ Pass to next middleware
});

// This runs for EVERY request!
app.get('/users', (req, res) => {
  res.send('Users list');
});
🔐
2. Authentication Middleware
Route-Specific

Checks if user is authenticated before allowing access:

// Authentication middleware
function requireAuth(req, res, next) {
  const token = req.headers.authorization;
  
  if (!token) {
    // ❌ No token - block request!
    return res.status(401).json({ 
      error: 'Unauthorized' 
    });
  }
  
  // ✅ Token exists - allow request
  req.user = { id: 123, name: 'Alice' };
  next();
}

// Protected route
app.get('/dashboard', requireAuth, (req, res) => {
  res.json({ 
    message: 'Welcome!',
    user: req.user // Added by middleware!
  });
});
âąī¸
3. Request Timing
Performance Monitoring

Measures how long each request takes:

app.use((req, res, next) => {
  const start = Date.now();
  
  // When response finishes
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - ${duration}ms`);
  });
  
  next();
});
âš ī¸
4. Error Handler (4 parameters!)
Error Handling

Error middleware has 4 parameters including err:

// Error handler (must have 4 params!)
app.use((err, req, res, next) => {
  console.error('Error:', err.message);
  res.status(500).json({ 
    error: err.message 
  });
});

// Trigger error in route
app.get('/error', (req, res, next) => {
  const error = new Error('Something went wrong!');
  next(error); // Pass error to error handler
});
🎨 Types of Middleware
🌍
Application-Level

Runs for all routes using app.use()

app.use(express.json());
app.use(logger);
app.use(cors());
đŸ›¤ī¸
Router-Level

Bound to specific router instances

const router = express.Router();
router.use(auth);
router.get('/admin', handler);
đŸŽ¯
Route-Specific

Runs only for specific routes

app.get('/admin', auth, (req, res) => {
  res.send('Admin page');
});
đŸ“Ļ
Built-in

Express's built-in middleware (we already used express.json() and express.text()!)

express.json()      // ← Parse JSON bodies
express.text()      // ← Parse text bodies
express.urlencoded()// ← Parse form data
express.static()    // ← Serve static files
🌐
Third-Party

npm packages you install

const cors = require('cors');
const helmet = require('helmet');
app.use(cors());
app.use(helmet());
âš ī¸
Error-Handling

Has 4 parameters (err, req, res, next)

app.use((err, req, res, next) => {
  res.status(500).json({error: err.message});
});
đŸ“Ļ Built-in Middleware Deep Dive
📄
1. express.json()

Parses incoming requests with JSON payloads. Makes req.body available as a JavaScript object.

const express = require('express');
const app = express();

app.use(express.json()); // Enable JSON parsing

app.post('/api/users', (req, res) => {
  // req.body is automatically parsed!
  console.log(req.body); // { name: 'Alice', age: 25 }
  res.json({ message: 'User created', data: req.body });
});

// Client sends:
// POST /api/users
// Content-Type: application/json
// Body: {"name": "Alice", "age": 25}
📝
2. express.text()

Parses incoming requests with plain text bodies. Makes req.body available as a string.

app.use(express.text());

app.post('/webhook', (req, res) => {
  console.log(req.body); // "Plain text message"
  res.send(`Received: ${req.body}`);
});

// Client sends:
// POST /webhook
// Content-Type: text/plain
// Body: Plain text message
📋
3. express.urlencoded()

Parses incoming requests with URL-encoded payloads (HTML form submissions). Makes req.body available as an object.

app.use(express.urlencoded({ extended: true }));

app.post('/login', (req, res) => {
  console.log(req.body); // { username: 'alice', password: '12345' }
  res.send(`Welcome ${req.body.username}`);
});

// Client sends (HTML form):
// POST /login
// Content-Type: application/x-www-form-urlencoded
// Body: username=alice&password=12345
💡 extended: true allows parsing rich objects and arrays. Use it by default.
🌐
4. express.static() - Serve Static Files

Serves static files like HTML, CSS, JavaScript, images directly from a folder. Perfect for serving your frontend!

📁 Project Structure:
my-app/
  ├── server.js
  └── public/              ← Static files folder
      ├── index.html
      ├── style.css
      ├── script.js
      └── images/
          └── logo.png
const express = require('express');
const app = express();

// Serve all files from 'public' folder
app.use(express.static('public'));

// Now users can access:
// http://localhost:3000/index.html
// http://localhost:3000/style.css
// http://localhost:3000/script.js
// http://localhost:3000/images/logo.png

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
đŸŽ¯ Real Example: Website with Frontend

Create a simple website:

📄 public/index.html
<!DOCTYPE html>
<html>
<head>
  <title>My Website</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <h1>Welcome to My Website!</h1>
  <img src="/images/logo.png" alt="Logo">
  <script src="/script.js"></script>
</body>
</html>
🎨 public/style.css
body {
  font-family: Arial, sans-serif;
  background: #f0f0f0;
  margin: 0;
  padding: 20px;
}

h1 {
  color: #333;
}
⚡ public/script.js
console.log('Website loaded!');

document.querySelector('h1').addEventListener('click', () => {
  alert('Hello from client-side JavaScript!');
});
đŸ–Ĩī¸ server.js
const express = require('express');
const app = express();

// Serve static files from 'public' folder
app.use(express.static('public'));

// API endpoint (separate from static files)
app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello from API!' });
});

app.listen(3000, () => {
  console.log('Server: http://localhost:3000');
});
💡 express.static() Tips
  • Virtual Path: Mount at a specific URL path
app.use('/assets', express.static('public'));
// Files accessible at: http://localhost:3000/assets/style.css
  • Multiple Folders: Serve from multiple directories
app.use(express.static('public'));
app.use(express.static('uploads'));
// Express checks 'public' first, then 'uploads'
  • Cache Control: Set cache headers for performance
app.use(express.static('public', {
  maxAge: '1d', // Cache for 1 day
  etag: true     // Enable ETag headers
}));
âš ī¸
Order Matters!

Middleware executes in the order you define it. Remember express.json() we learned earlier? It's a middleware! Always put it BEFORE routes that need to read req.body.

✅ Correct Order
const express = require('express');
const app = express();

app.use(express.json());  // ✅ Parse body first!

app.post('/users', (req, res) => {
  console.log(req.body);  // ✅ Works! Body is parsed
  res.json({ received: req.body });
});
❌ Wrong Order
const express = require('express');
const app = express();

app.post('/users', (req, res) => {
  console.log(req.body);  // ❌ undefined!
  res.json({ received: req.body });
});

app.use(express.json());  // ❌ Too late!
đŸŽ¯ Complete Middleware Example
const express = require('express');
const app = express();

// 1ī¸âƒŖ Application-level middleware (runs for ALL requests)
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // ✅ Pass control to next middleware
});

// 2ī¸âƒŖ Built-in middleware
app.use(express.json()); // Parse JSON bodies

// 3ī¸âƒŖ Custom authentication middleware
function requireAuth(req, res, next) {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  req.user = { id: 123, name: 'Alice' }; // Attach user to request
  next(); // ✅ Authenticated, continue
}

// 4ī¸âƒŖ Public route (no middleware)
app.get('/', (req, res) => {
  res.send('Welcome to the API!');
});

// 5ī¸âƒŖ Protected route (uses requireAuth middleware)
app.get('/dashboard', requireAuth, (req, res) => {
  res.json({ 
    message: 'Dashboard',
    user: req.user // Available thanks to middleware!
  });
});

// 6ī¸âƒŖ Multiple middleware in sequence
app.post('/admin', requireAuth, checkAdmin, (req, res) => {
  res.send('Admin panel');
});

function checkAdmin(req, res, next) {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }
  next(); // ✅ Is admin, continue
}

// 7ī¸âƒŖ Error-handling middleware (MUST be last!)
app.use((err, req, res, next) => {
  console.error('Error:', err.message);
  res.status(500).json({ error: 'Internal Server Error' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
🎓 Key Takeaways
  • Middleware are functions that process requests in a queue/pipeline
  • Always call next() unless you send a response
  • Order matters! Middleware runs top-to-bottom
  • Use middleware for logging, authentication, parsing, error handling
  • Error middleware has 4 parameters: (err, req, res, next)
  • Middleware can modify req and res for later use

🚨 Error Handling in Express

Proper error handling is crucial for building robust applications. Without it, your server crashes on unexpected errors, leaving users with no response. In this section, we'll learn how to catch errors, handle them gracefully, and keep your server running!

âš ī¸ Part 1: The Problem - Unhandled Errors Crash Your Server

Let's start by understanding what happens when errors are NOT handled properly:

const express = require('express');
const app = express();

app.use(express.json());

// In-memory data for testing
const todos = [
  { id: 1, task: 'Learn Node.js', completed: false },
  { id: 2, task: 'Build an API', completed: true }
];

// ❌ BAD: No error handling - this WILL crash your server!
app.get('/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const todo = todos.find(t => t.id === id);
  
  // What if todo is undefined? đŸ’Ĩ CRASH!
  res.json({
    task: todo.task,        // ❌ Cannot read property 'task' of undefined
    completed: todo.completed
  });
});

app.post('/todos', (req, res) => {
  const { task } = req.body;
  
  // What if task is missing? đŸ’Ĩ CRASH!
  const newTodo = {
    id: todos.length + 1,
    task: task.trim(),  // ❌ Cannot read property 'trim' of undefined
    completed: false
  };
  
  todos.push(newTodo);
  res.json(newTodo);
});

app.listen(3000, () => console.log('Server on port 3000'));

// Try these requests:
// GET /todos/999  →  đŸ’Ĩ Server crashes! (todo is undefined)
// POST /todos with empty body  →  đŸ’Ĩ Server crashes! (task is undefined)
đŸ’Ĩ
What Happens When It Crashes?

Without error handling:
â€ĸ Your entire server stops running
â€ĸ Users get no response (request hangs)
â€ĸ You have to manually restart the server
â€ĸ All users lose connection

This is unacceptable in production! Your server must handle errors gracefully and stay alive.

✅ Part 2: Solution 1 - Try-Catch (Works, But Repetitive)

The first solution is to wrap your code in try-catch blocks. This prevents crashes, but you'll see the problem: it gets repetitive fast!

const express = require('express');
const app = express();

app.use(express.json());

const todos = [
  { id: 1, task: 'Learn Node.js', completed: false },
  { id: 2, task: 'Build an API', completed: true }
];

// ✅ BETTER: Handle errors with try-catch
app.get('/todos/:id', (req, res) => {
  try {
    const id = parseInt(req.params.id);
    
    // Validate ID
    if (isNaN(id)) {
      return res.status(400).json({ error: 'Invalid ID format' });
    }
    
    const todo = todos.find(t => t.id === id);
    
    // Check if todo exists BEFORE using it
    if (!todo) {
      return res.status(404).json({ error: 'Todo not found' });
    }
    
    res.json(todo);
  } catch (error) {
    console.error('Error:', error.message);
    res.status(500).json({ error: 'Something went wrong' });
  }
});

// ✅ BETTER: But we need try-catch in EVERY route!
app.post('/todos', (req, res) => {
  try {  // âš ī¸ See the repetition?
    const { task } = req.body;
    
    // Validation
    if (!task || task.trim() === '') {
      return res.status(400).json({ error: 'Task is required' });
    }
    
    if (task.length > 100) {
      return res.status(400).json({ error: 'Task too long (max 100 chars)' });
    }
    
    const newTodo = {
      id: todos.length + 1,
      task: task.trim(),
      completed: false
    };
    
    todos.push(newTodo);
    res.status(201).json(newTodo);
  } catch (error) {  // âš ī¸ Same code again!
    console.error('Error:', error.message);
    res.status(500).json({ error: 'Failed to create todo' });
  }
});

app.delete('/todos/:id', (req, res) => {
  try {  // âš ī¸ And again!
    const id = parseInt(req.params.id);
    
    if (isNaN(id)) {
      return res.status(400).json({ error: 'Invalid ID' });
    }
    
    const index = todos.findIndex(t => t.id === id);
    
    if (index === -1) {
      return res.status(404).json({ error: 'Todo not found' });
    }
    
    todos.splice(index, 1);
    res.json({ message: 'Todo deleted' });
  } catch (error) {  // âš ī¸ Repetitive!
    console.error('Error:', error.message);
    res.status(500).json({ error: 'Failed to delete' });
  }
});

app.listen(3000);
😩
The Problem with This Approach

Pros: ✅ Server doesn't crash, errors are handled
Cons: ❌ Lots of repetitive code (try-catch in EVERY route)

Notice how we're writing the same try-catch pattern in every single route? This violates the DRY principle (Don't Repeat Yourself). There's a better way!

đŸŽ¯ Part 3: Solution 2 - asyncHandler Wrapper (Best Practice! ⭐)

Instead of repeating try-catch in every route, we create a reusable wrapper function that automatically catches errors. Write the error handling logic ONCE, use it everywhere!

🧠 Understanding the Wrapper (Step-by-Step)
💡
The Core Concept

A Higher-Order Function: A function that takes another function as input and returns a new function.

Our asyncHandler takes your route handler function, wraps it in try-catch, and returns this wrapped version to Express. Express then calls the wrapped function automatically!

// Step 1: The asyncHandler function
// This is the "wrapper" - you write it ONCE

const asyncHandler = (fn) => {
  // fn = your route handler function
  
  // Return a NEW function that Express will call
  return async (req, res, next) => {
    try {
      // Run your route handler
      await fn(req, res, next);
    } catch (error) {
      // If any error happens, pass it to error middleware
      next(error);
    }
  };
};

// Step 2: How to use it
// Instead of writing try-catch yourself:
app.get('/todos/:id', async (req, res) => {
  try {
    // your code...
  } catch (error) {
    // handle error...
  }
});

// Wrap your function with asyncHandler:
app.get('/todos/:id', asyncHandler(async (req, res) => {
  // Just write your code! No try-catch needed!
  // If error happens, asyncHandler catches it automatically
}));
🔍
Breaking It Down Line by Line

Line 1: asyncHandler takes fn (your async route handler)
Line 2: Returns a NEW async function with (req, res, next)
Line 3: try block runs your function: await fn(req, res, next)
Line 4: catch block catches any error and calls next(error)
Result: Express receives the wrapped function and calls it when requests arrive!

📊 Visual Comparison: Before vs After
// ❌ WITHOUT asyncHandler: Repetitive code

app.get('/todos/:id', async (req, res) => {
  try {  // đŸ˜Ģ Repetition
    const id = parseInt(req.params.id);
    const todo = todos.find(t => t.id === id);
    if (!todo) return res.status(404).json({ error: 'Not found' });
    res.json(todo);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/todos', async (req, res) => {
  try {  // đŸ˜Ģ Repetition
    const { task } = req.body;
    if (!task) throw new Error('Task required');
    const newTodo = { id: todos.length + 1, task };
    todos.push(newTodo);
    res.json(newTodo);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.delete('/todos/:id', async (req, res) => {
  try {  // đŸ˜Ģ Repetition
    const id = parseInt(req.params.id);
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) return res.status(404).json({ error: 'Not found' });
    todos.splice(index, 1);
    res.json({ message: 'Deleted' });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// You're writing try-catch in EVERY route! 😩
// ✅ WITH asyncHandler: Clean and DRY!

const asyncHandler = (fn) => {
  return async (req, res, next) => {
    try {
      await fn(req, res, next);
    } catch (error) {
      next(error);
    }
  };
};

// Now all routes are clean - errors caught automatically!
app.get('/todos/:id', asyncHandler(async (req, res) => {
  const id = parseInt(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) return res.status(404).json({ error: 'Not found' });
  res.json(todo);
}));

app.post('/todos', asyncHandler(async (req, res) => {
  const { task } = req.body;
  if (!task) throw new Error('Task required');  // Caught by wrapper!
  const newTodo = { id: todos.length + 1, task };
  todos.push(newTodo);
  res.json(newTodo);
}));

app.delete('/todos/:id', asyncHandler(async (req, res) => {
  const id = parseInt(req.params.id);
  const index = todos.findIndex(t => t.id === id);
  if (index === -1) return res.status(404).json({ error: 'Not found' });
  todos.splice(index, 1);
  res.json({ message: 'Deleted' });
}));

// 😊 Much cleaner! Write try-catch ONCE, use everywhere!
đŸŽ¯
Why This is Better

DRY Principle: Don't Repeat Yourself - error handling in ONE place
Cleaner Routes: Focus on business logic, not error handling boilerplate
Consistency: All routes handle errors the same way
Maintainable: Change error handling once, affects all routes
Automatic: Just wrap your function, errors are caught automatically!

🔄
Key Insight: Where Errors Are Caught

The Error Flow:

1ī¸âƒŖ You throw an error in your route
2ī¸âƒŖ asyncHandler's catch block catches it
3ī¸âƒŖ Calls next(error) to pass error to Express
4ī¸âƒŖ Express sends it to error middleware
5ī¸âƒŖ Error middleware sends response to client
6ī¸âƒŖ Server keeps running! ✅

The server only crashes if errors are NOT caught. asyncHandler ensures ALL errors are caught!

đŸ›Ąī¸ Part 4: Error Handling Middleware (Centralized Error Responses)

Now that asyncHandler catches errors and passes them via next(error), we need error middleware to send proper responses to clients. This is where we handle ALL errors in ONE place!

⚡
Critical Rules for Error Middleware

1. Must have 4 parameters: (err, req, res, next) - Express recognizes this as error middleware
2. Must be defined LAST: After all routes and other middleware
3. Catches all errors: From asyncHandler, other middleware, or manual next(error) calls

// Basic error middleware
app.use((err, req, res, next) => {
  // Log the error
  console.error('Error:', {
    message: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method
  });
  
  // Send error response
  res.status(500).json({
    error: err.message || 'Internal Server Error'
  });
});

// Better: Environment-aware error middleware
app.use((err, req, res, next) => {
  console.error('❌ Error:', err.message);
  
  // In development: show full error details
  if (process.env.NODE_ENV === 'development') {
    res.status(err.statusCode || 500).json({
      error: err.message,
      stack: err.stack,  // Show stack trace in dev
      path: req.path
    });
  } else {
    // In production: hide sensitive details
    res.status(err.statusCode || 500).json({
      error: err.message || 'Something went wrong'
    });
  }
});

// Best: Advanced error middleware with 404 handler
const express = require('express');
const app = express();

// ... your routes ...

// 404 handler (no route matched)
app.use((req, res, next) => {
  res.status(404).json({
    error: 'Route not found',
    path: req.path
  });
});

// Error handler (MUST be LAST)
app.use((err, req, res, next) => {
  // Set default status code
  const statusCode = err.statusCode || 500;
  
  // Log error details
  console.error(`[${new Date().toISOString()}] Error:`, {
    message: err.message,
    statusCode,
    path: req.path,
    method: req.method
  });
  
  // Send appropriate response
  res.status(statusCode).json({
    success: false,
    error: process.env.NODE_ENV === 'development' 
      ? err.message 
      : 'An error occurred',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
});
īŋŊ
Understanding the Syntax: ...(condition && { property })

You might have noticed this unusual syntax above:
...(process.env.NODE_ENV === 'development' && { stack: err.stack })

This is called "Conditional Property Inclusion" using the spread operator. Here's how it works:

1. The && operator here is NOT about comparison!
â€ĸ In JavaScript, && returns the first falsy value OR the last value if all are truthy
â€ĸ true && { stack: 'error' } → returns { stack: 'error' }
â€ĸ false && { stack: 'error' } → returns false

2. The spread operator ... then spreads the result:
â€ĸ If condition is true: ...{ stack: err.stack } → adds stack: err.stack to the object
â€ĸ If condition is false: ...false → spreads nothing (ignored by JavaScript)

Example:
const isDev = true;
const obj = { name: 'error', ...(isDev && { details: 'info' }) };
// Result: { name: 'error', details: 'info' }

const isProd = false;
const obj = { name: 'error', ...(isProd && { details: 'info' }) };
// Result: { name: 'error' } (details property not added)

Why use this pattern?
â€ĸ Conditionally add properties to objects without if-else statements
â€ĸ Keeps code concise and readable
â€ĸ Common in modern JavaScript/Node.js codebases

īŋŊ💡
Why Centralized Error Handling?

Consistency: All errors formatted the same way
Maintainability: Change error responses in ONE place
Logging: Log all errors from one location
Security: Control what error details are exposed
Environment-aware: Different behavior for dev vs production

✨ Part 5: Custom Error Classes (Enhancement, Not a Solution!)

Important: Custom error classes are NOT a catching solution! They help you CREATE better errors with proper status codes and messages. They work best when combined with asyncHandler from Part 3.

âš ī¸
Common Misconception

Custom error classes don't catch errors - they're just special error objects with extra properties (like statusCode). You still need asyncHandler or try-catch to CATCH them!

Think of it this way:
â€ĸ asyncHandler = catches errors ✅
â€ĸ Custom Error Classes = makes errors better ✨
â€ĸ Together = clean code + meaningful errors! 🚀

đŸ“Ļ Creating Custom Error Classes
// errors/AppError.js - Base error class
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true; // Mark as expected/operational error
    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = AppError;

// errors/NotFoundError.js - For 404 errors
const AppError = require('./AppError');

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404);
  }
}

module.exports = NotFoundError;

// errors/ValidationError.js - For 400 bad request errors
const AppError = require('./AppError');

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

module.exports = ValidationError;
đŸšĢ Wrong Way: Using try-catch Everywhere (Still Repetitive!)
// ❌ DON'T DO THIS - Still repetitive like Solution 1!

const todos = [
  { id: 1, task: 'Learn Node.js', completed: false },
  { id: 2, task: 'Build an API', completed: true }
];

app.get('/todos/:id', (req, res, next) => {
  try {  // âš ī¸ Repetitive!
    const id = parseInt(req.params.id);
    
    if (isNaN(id)) {
      throw new ValidationError('ID must be a number');
    }
    
    const todo = todos.find(t => t.id === id);
    
    if (!todo) {
      throw new NotFoundError('Todo');
    }
    
    res.json(todo);
  } catch (error) {
    next(error);
  }
});

app.post('/todos', (req, res, next) => {
  try {  // âš ī¸ Repetitive!
    const { task } = req.body;
    
    if (!task || task.trim() === '') {
      throw new ValidationError('Task cannot be empty');
    }
    
    const newTodo = {
      id: todos.length + 1,
      task: task.trim(),
      completed: false
    };
    
    todos.push(newTodo);
    res.status(201).json(newTodo);
  } catch (error) {
    next(error);
  }
});

// Problem: You're writing try-catch in EVERY route. This is Solution 1!
✅ Right Way: Combine with asyncHandler (Clean + Meaningful Errors)!

Use custom error classes WITH asyncHandler from Solution 2 – you get clean code AND meaningful error messages!

// ✅ BEST APPROACH: Custom Errors + asyncHandler
// You get: Clean routes + Specific errors + Proper status codes

const asyncHandler = (fn) => {
  return async (req, res, next) => {
    try {
      await fn(req, res, next);
    } catch (error) {
      next(error);  // Catches thrown errors automatically!
    }
  };
};

// Now your routes are super clean:
app.get('/todos/:id', asyncHandler(async (req, res) => {
  const id = parseInt(req.params.id);
  
  // Just throw! asyncHandler catches it
  if (isNaN(id)) {
    throw new ValidationError('ID must be a number');
  }
  
  const todo = todos.find(t => t.id === id);
  
  if (!todo) {
    throw new NotFoundError('Todo');  // asyncHandler catches this too!
  }
  
  res.json(todo);
}));

app.post('/todos', asyncHandler(async (req, res) => {
  const { task } = req.body;
  
  // All these throws are caught by asyncHandler!
  if (!task || task.trim() === '') {
    throw new ValidationError('Task cannot be empty');
  }
  
  if (task.length > 100) {
    throw new ValidationError('Task too long');
  }
  
  const newTodo = {
    id: todos.length + 1,
    task: task.trim(),
    completed: false
  };
  
  todos.push(newTodo);
  res.status(201).json(newTodo);
}));

// Error middleware receives all errors
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    success: false,
    error: err.message
  });
});
đŸŽ¯
Summary: How to Handle Errors Properly

đŸ›Ąī¸ The 2 Real Solutions for Catching Errors: ❌ Solution 1: Try-catch in every route → Works but repetitive ✅ Solution 2: asyncHandler wrapper → Best! DRY principle Custom Error Classes are NOT a catching solution: They're an enhancement that makes your errors better (status codes, messages). 🚀 Best Practice: asyncHandler (Part 3) + Custom Error Classes (Part 5) = Clean code with meaningful errors! 🎉

đŸŽ¯ Part 6: Complete Production-Ready Example

Here's everything together: asyncHandler + Custom Error Classes + Error Middleware + 404 Handler. This is production-ready code!

// server.js - Complete production-ready error handling
const express = require('express');
const app = express();

app.use(express.json());

// 1ī¸âƒŖ Custom Error Classes
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;  // Mark as expected error
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, 404);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

// 2ī¸âƒŖ asyncHandler Wrapper
const asyncHandler = (fn) => {
  return async (req, res, next) => {
    try {
      await fn(req, res, next);
    } catch (error) {
      next(error);
    }
  };
};

// 3ī¸âƒŖ Sample Data
const todos = [
  { id: 1, task: 'Learn Express', completed: false },
  { id: 2, task: 'Build API', completed: true }
];

// 4ī¸âƒŖ Routes - Clean with asyncHandler + Custom Errors
app.get('/todos', asyncHandler(async (req, res) => {
  res.json(todos);
}));

app.get('/todos/:id', asyncHandler(async (req, res) => {
  const id = parseInt(req.params.id);
  
  if (isNaN(id)) {
    throw new ValidationError('ID must be a valid number');
  }
  
  const todo = todos.find(t => t.id === id);
  
  if (!todo) {
    throw new NotFoundError('Todo');
  }
  
  res.json(todo);
}));

app.post('/todos', asyncHandler(async (req, res) => {
  const { task } = req.body;
  
  if (!task || task.trim() === '') {
    throw new ValidationError('Task is required');
  }
  
  if (task.length > 100) {
    throw new ValidationError('Task too long (max 100 characters)');
  }
  
  const newTodo = {
    id: todos.length + 1,
    task: task.trim(),
    completed: false
  };
  
  todos.push(newTodo);
  res.status(201).json(newTodo);
}));

// 5ī¸âƒŖ 404 Handler (no route matched)
app.use((req, res) => {
  res.status(404).json({
    success: false,
    error: 'Route not found',
    path: req.path
  });
});

// 6ī¸âƒŖ Error Middleware (MUST be LAST and have 4 parameters!)
// Note: app.use() has a special overload that accepts (err, req, res, next)
// When Express sees 4 parameters, it treats this as error handling middleware
app.use((err, req, res, next) => {
  // Use statusCode from custom error or default to 500
  const statusCode = err.statusCode || 500;
  
  // Log error
  console.error(`[${statusCode}] ${err.message}`);
  
  // Send response
  res.status(statusCode).json({
    success: false,
    error: err.message || 'Internal Server Error'
  });
});

app.listen(3000, () => console.log('✅ Server running on port 3000'));

// Test it:
// GET  /todos              →  200: Returns all todos
// GET  /todos/1            →  200: Returns todo 1
// GET  /todos/999          →  404: Todo not found
// GET  /todos/abc          →  400: ID must be a valid number
// POST /todos (no body)    →  400: Task is required
// GET  /invalid-route      →  404: Route not found
🚀
What Makes This Production-Ready?

✅ asyncHandler: No repetitive try-catch, errors caught automatically ✅ Custom Errors: Proper HTTP status codes (400, 404, 500) ✅ Centralized Middleware: All errors handled in ONE place ✅ 404 Handler: Catches invalid routes ✅ Environment-aware: Stack traces only in development ✅ Consistent Format: All responses structured the same ✅ Server Stability: Never crashes, always returns proper response

🔑 Key Takeaways

  • 📌 Part 1: Unhandled errors crash your server - unacceptable in production!
  • 📌 Part 2: Try-catch works but creates repetitive code
  • 📌 Part 3: asyncHandler wrapper = DRY principle, write error handling ONCE
  • 📌 Part 4: Error middleware catches all errors in ONE central place
  • 📌 Part 5: Custom error classes enhance errors with proper status codes
  • ⭐ đŸŽ¯ Best Practice: asyncHandler + Custom Errors + Error Middleware = Production-ready!

✅ Input Validation

Never trust user input! Validation ensures data is correct, complete, and safe before processing. This prevents bugs, crashes, and data corruption.

🤔 Why Validate Input?

🐛
Prevent Bugs

Invalid data causes crashes and unexpected behavior

💾
Data Integrity

Keep your database clean and consistent

👤
Better UX

Give users clear, actionable error messages

📏
Enforce Rules

Ensure data meets business requirements

🔧 Manual Validation

// Basic manual validation
app.post('/users', (req, res) => {
  const { name, email, age } = req.body;
  const errors = [];
  
  // Check required fields
  if (!name || name.trim() === '') {
    errors.push('Name is required');
  }
  
  if (!email || email.trim() === '') {
    errors.push('Email is required');
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    errors.push('Email must be valid');
  }
  
  // Check types and ranges
  if (age !== undefined) {
    const ageNum = parseInt(age);
    if (isNaN(ageNum)) {
      errors.push('Age must be a number');
    } else if (ageNum < 0 || ageNum > 150) {
      errors.push('Age must be between 0 and 150');
    }
  }
  
  // Check length
  if (name && name.length > 100) {
    errors.push('Name must be less than 100 characters');
  }
  
  // Return errors if any
  if (errors.length > 0) {
    return res.status(400).json({
      success: false,
      errors
    });
  }
  
  // Create user
  const newUser = { name: name.trim(), email: email.trim(), age };
  res.status(201).json({ success: true, data: newUser });
});
âš ī¸
The Problem with Manual Validation

Manual validation becomes messy and repetitive quickly. For complex applications, use a validation library!

đŸ“Ļ express-validator Library

express-validator is the most popular validation library for Express. It's built on top of validator.js and provides a clean, middleware-based API.

# Install express-validator
npm install express-validator
đŸŽ¯ Basic Usage
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();

app.use(express.json());

// POST /users with validation
app.post('/users',
  // Validation middleware
  body('name')
    .trim()
    .notEmpty().withMessage('Name is required')
    .isLength({ min: 2, max: 100 }).withMessage('Name must be 2-100 characters'),
  
  body('email')
    .trim()
    .notEmpty().withMessage('Email is required')
    .isEmail().withMessage('Email must be valid')
    .normalizeEmail(),
  
  body('age')
    .optional()
    .isInt({ min: 0, max: 150 }).withMessage('Age must be between 0 and 150')
    .toInt(),
  
  // Route handler
  (req, res) => {
    // Check for validation errors
    const errors = validationResult(req);
    
    if (!errors.isEmpty()) {
      return res.status(400).json({
        success: false,
        errors: errors.array()
      });
    }
    
    // Data is valid!
    const { name, email, age } = req.body;
    res.status(201).json({
      success: true,
      data: { name, email, age }
    });
  }
);

app.listen(3000);
📋 Common Validation Rules
notEmpty()

Field must not be empty

isEmail()

Must be valid email

isInt()

Must be integer

isLength()

Check string length

isURL()

Must be valid URL

isStrongPassword()

Check password strength

isDate()

Must be valid date

isBoolean()

Must be boolean

matches()

Match regex pattern

isIn()

Value in allowed list

custom()

Custom validation logic

optional()

Field is optional

â™ģī¸ Reusable Validation Middleware
// validators/userValidators.js
const { body, param, validationResult } = require('express-validator');

// Reusable validation rules
const userValidationRules = () => {
  return [
    body('name')
      .trim()
      .notEmpty().withMessage('Name is required')
      .isLength({ min: 2, max: 100 }).withMessage('Name must be 2-100 characters'),
    
    body('email')
      .trim()
      .notEmpty().withMessage('Email is required')
      .isEmail().withMessage('Email must be valid')
      .normalizeEmail(),
    
    body('age')
      .optional()
      .isInt({ min: 0, max: 150 }).withMessage('Age must be between 0 and 150')
      .toInt()
  ];
};

// Middleware to check validation result
const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      success: false,
      errors: errors.array().map(err => ({
        field: err.path,
        message: err.msg
      }))
    });
  }
  next();
};

module.exports = {
  userValidationRules,
  validate
};

// routes/users.js - Clean usage
const { userValidationRules, validate } = require('../validators/userValidators');

app.post('/users', userValidationRules(), validate, (req, res) => {
  // Data is validated!
  const { name, email, age } = req.body;
  res.status(201).json({ success: true, data: { name, email, age } });
});
đŸ”Ĩ Advanced Validation Examples
const { body, param } = require('express-validator');

// Custom validation function
const userExists = async (value) => {
  const user = await database.findUserByEmail(value);
  if (user) {
    throw new Error('Email already exists');
  }
  return true;
};

// Password confirmation
const passwordMatch = (value, { req }) => {
  if (value !== req.body.password) {
    throw new Error('Passwords do not match');
  }
  return true;
};

// Registration endpoint with advanced validation
app.post('/register',
  body('email')
    .isEmail().withMessage('Invalid email')
    .normalizeEmail()
    .custom(userExists).withMessage('Email already registered'),
  
  body('password')
    .isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
    .matches(/\d/).withMessage('Password must contain a number')
    .matches(/[A-Z]/).withMessage('Password must contain uppercase letter'),
  
  body('confirmPassword')
    .custom(passwordMatch).withMessage('Passwords do not match'),
  
  body('role')
    .optional()
    .isIn(['user', 'admin', 'moderator']).withMessage('Invalid role'),
  
  validate,
  
  async (req, res) => {
    const { email, password, role } = req.body;
    // Create user...
    res.status(201).json({ success: true });
  }
);

// Validate URL parameters
app.get('/users/:id',
  param('id')
    .isInt().withMessage('User ID must be an integer')
    .toInt(),
  
  validate,
  
  (req, res) => {
    const userId = req.params.id; // Already converted to integer
    res.json({ id: userId });
  }
);

🔑 Key Takeaways

  • Always validate user input before processing
  • Use express-validator for clean, maintainable validation
  • Return clear, specific error messages to users
  • Validate data types, ranges, formats, and business rules
  • Create reusable validation middleware
  • Sanitize input (trim, normalize) alongside validation

📊 Logging Best Practices

Good logging is essential for debugging, monitoring, and understanding your application's behavior. console.log() is fine for learning, but production apps need structured, level-based logging.

🤔 Why Move Beyond console.log()?

❌
No Log Levels

Can't distinguish between info, warnings, and errors

❌
No Structure

Hard to parse and search logs automatically

❌
No Persistence

Logs disappear when server restarts

❌
Production Issues

Can't filter or control logging in production

📊 Log Levels Explained

🔴 error

Critical issues that need immediate attention. Server errors, crashes, failures.

logger.error('Database connection failed');
🟡 warn

Potential issues or deprecated features. Not critical but should be investigated.

logger.warn('API rate limit approaching');
đŸ”ĩ info

Important events: server start, user actions, successful operations.

logger.info('Server started on port 3000');
đŸŸŖ debug

Detailed information for debugging. Only active in development.

logger.debug('Query executed:', query);

đŸ“Ļ Winston: Professional Logging

Winston is the most popular logging library for Node.js. It supports multiple transports (console, file, cloud), log levels, and formatting.

# Install winston
npm install winston
đŸŽ¯ Basic Winston Setup
// logger.js
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info', // Minimum level to log
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    // Write to console
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),
    // Write to file
    new winston.transports.File({ 
      filename: 'logs/error.log', 
      level: 'error' 
    }),
    new winston.transports.File({ 
      filename: 'logs/combined.log' 
    })
  ]
});

module.exports = logger;

// Usage in your app
const logger = require('./logger');

logger.info('Server starting...');
logger.error('Database connection failed', { error: err.message });
logger.warn('High memory usage detected');
logger.debug('Request payload:', req.body);
🚀 Production-Ready Logger
// config/logger.js
const winston = require('winston');

const isDevelopment = process.env.NODE_ENV === 'development';

const logger = winston.createLogger({
  level: isDevelopment ? 'debug' : 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'api-server' },
  transports: [
    // Console output (pretty in dev, JSON in prod)
    new winston.transports.Console({
      format: isDevelopment
        ? winston.format.combine(
            winston.format.colorize(),
            winston.format.printf(({ timestamp, level, message, ...meta }) => {
              return `${timestamp} [${level}]: ${message} ${
                Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
              }`;
            })
          )
        : winston.format.json()
    }),
    
    // File outputs
    new winston.transports.File({ 
      filename: 'logs/error.log', 
      level: 'error',
      maxsize: 5242880, // 5MB
      maxFiles: 5
    }),
    new winston.transports.File({ 
      filename: 'logs/combined.log',
      maxsize: 5242880,
      maxFiles: 5
    })
  ]
});

module.exports = logger;

// app.js - Use in Express
const express = require('express');
const logger = require('./config/logger');

const app = express();

// Log every request
app.use((req, res, next) => {
  logger.info(`${req.method} ${req.path}`, {
    ip: req.ip,
    userAgent: req.get('user-agent')
  });
  next();
});

// Use in routes
app.get('/users', async (req, res) => {
  try {
    logger.debug('Fetching users from database');
    const users = await database.getUsers();
    logger.info(`Retrieved ${users.length} users`);
    res.json(users);
  } catch (error) {
    logger.error('Failed to fetch users', {
      error: error.message,
      stack: error.stack
    });
    res.status(500).json({ error: 'Failed to fetch users' });
  }
});

// Log unhandled errors
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Rejection:', { reason, promise });
});

process.on('uncaughtException', (error) => {
  logger.error('Uncaught Exception:', { error: error.message, stack: error.stack });
  process.exit(1);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  logger.info(`Server started successfully`, { port: PORT, env: process.env.NODE_ENV });
});

📝 What to Log (and What NOT to Log)

✅ DO Log
  • Server start/stop events
  • Incoming requests (method, path, IP)
  • Database operations
  • External API calls
  • Authentication attempts (success/failure)
  • Errors with context
  • Performance metrics
  • Business-critical events
❌ DON'T Log
  • 🔒 Passwords (EVER!)
  • 🔑 API keys or tokens
  • đŸ’ŗ Credit card numbers
  • 🔐 Session IDs
  • 📱 Personal identifiable information (PII)
  • 🔒 Security answers
  • 📧 Full email contents
  • đŸ—ī¸ Encryption keys
âš ī¸
Security Warning

Logging sensitive data can lead to security breaches! Attackers often gain access through log files. Always sanitize data before logging.

🔑 Key Takeaways

  • Use winston or pino instead of console.log in production
  • Use appropriate log levels (error, warn, info, debug)
  • Log to files with rotation to prevent disk space issues
  • Include timestamps and context in logs
  • Different logging config for dev vs production
  • NEVER log passwords, tokens, or sensitive data
  • Structured logging (JSON) makes logs searchable

📤 API Response Patterns

Consistent API responses make your API easier to use and maintain. Clients know exactly what to expect, reducing bugs and improving developer experience.

🤔 Why Consistent Responses Matter

đŸŽ¯
Predictable

Clients know what structure to expect every time

🐛
Fewer Bugs

Consistent format reduces parsing errors

📚
Self-Documenting

Clear response structure is easy to understand

🔧
Easy to Maintain

Standardized responses simplify refactoring

✅ Standard Response Format

// Standard success response
{
  "success": true,
  "data": { /* your data here */ },
  "message": "Optional success message"
}

// Standard error response
{
  "success": false,
  "error": "Error message",
  "errors": [ /* optional array of detailed errors */ ]
}

// Paginated response
{
  "success": true,
  "data": [ /* array of items */ ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 100,
    "totalPages": 10
  }
}

🔧 Response Helper Functions

// utils/response.js
class ApiResponse {
  static success(res, data, message = null, statusCode = 200) {
    return res.status(statusCode).json({
      success: true,
      data,
      ...(message && { message })
    });
  }

  static error(res, message, statusCode = 500, errors = null) {
    return res.status(statusCode).json({
      success: false,
      error: message,
      ...(errors && { errors })
    });
  }

  static paginated(res, data, page, limit, total) {
    return res.status(200).json({
      success: true,
      data,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total,
        totalPages: Math.ceil(total / limit),
        hasNext: page * limit < total,
        hasPrev: page > 1
      }
    });
  }

  static created(res, data, message = 'Resource created successfully') {
    return this.success(res, data, message, 201);
  }

  static noContent(res) {
    return res.status(204).send();
  }

  static badRequest(res, message, errors = null) {
    return this.error(res, message, 400, errors);
  }

  static unauthorized(res, message = 'Unauthorized') {
    return this.error(res, message, 401);
  }

  static forbidden(res, message = 'Forbidden') {
    return this.error(res, message, 403);
  }

  static notFound(res, message = 'Resource not found') {
    return this.error(res, message, 404);
  }
}

module.exports = ApiResponse;

// Usage in routes
const ApiResponse = require('./utils/response');

// Success
app.get('/users', async (req, res) => {
  const users = await database.getUsers();
  return ApiResponse.success(res, users);
});

// Created
app.post('/users', async (req, res) => {
  const user = await database.createUser(req.body);
  return ApiResponse.created(res, user);
});

// Not found
app.get('/users/:id', async (req, res) => {
  const user = await database.getUser(req.params.id);
  if (!user) {
    return ApiResponse.notFound(res, 'User not found');
  }
  return ApiResponse.success(res, user);
});

// Validation errors
app.post('/users', validate, async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return ApiResponse.badRequest(res, 'Validation failed', errors.array());
  }
  // ...
});

📊 HTTP Status Codes Reference

2xx Success
200 OK Request successful
201 Created Resource created
204 No Content Success, no content
4xx Client Errors
400 Bad Request Invalid request
401 Unauthorized Not authenticated
403 Forbidden No permission
404 Not Found Resource not found
422 Unprocessable Validation failed
5xx Server Errors
500 Internal Error Server error
503 Service Unavailable Server down

đŸŽ¯ Complete API Example with Best Practices

// Complete REST API with proper responses
const express = require('express');
const { body, validationResult } = require('express-validator');
const ApiResponse = require('./utils/response');
const asyncHandler = require('./utils/asyncHandler');

const app = express();
app.use(express.json());

// Mock database
let todos = [
  { id: 1, title: 'Learn Node.js', completed: false },
  { id: 2, title: 'Build API', completed: false }
];
let nextId = 3;

// GET /todos - List with pagination
app.get('/todos', asyncHandler(async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const start = (page - 1) * limit;
  const end = start + limit;
  
  const paginatedTodos = todos.slice(start, end);
  
  return ApiResponse.paginated(res, paginatedTodos, page, limit, todos.length);
}));

// GET /todos/:id - Get single todo
app.get('/todos/:id', asyncHandler(async (req, res) => {
  const todo = todos.find(t => t.id === parseInt(req.params.id));
  
  if (!todo) {
    return ApiResponse.notFound(res, 'Todo not found');
  }
  
  return ApiResponse.success(res, todo);
}));

// POST /todos - Create todo
app.post('/todos',
  body('title').trim().notEmpty().withMessage('Title is required'),
  asyncHandler(async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return ApiResponse.badRequest(res, 'Validation failed', errors.array());
    }
    
    const newTodo = {
      id: nextId++,
      title: req.body.title,
      completed: false
    };
    
    todos.push(newTodo);
    return ApiResponse.created(res, newTodo, 'Todo created successfully');
  })
);

// PUT /todos/:id - Update todo
app.put('/todos/:id',
  body('title').optional().trim().notEmpty(),
  body('completed').optional().isBoolean(),
  asyncHandler(async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return ApiResponse.badRequest(res, 'Validation failed', errors.array());
    }
    
    const todo = todos.find(t => t.id === parseInt(req.params.id));
    
    if (!todo) {
      return ApiResponse.notFound(res, 'Todo not found');
    }
    
    if (req.body.title !== undefined) todo.title = req.body.title;
    if (req.body.completed !== undefined) todo.completed = req.body.completed;
    
    return ApiResponse.success(res, todo, 'Todo updated successfully');
  })
);

// DELETE /todos/:id - Delete todo
app.delete('/todos/:id', asyncHandler(async (req, res) => {
  const index = todos.findIndex(t => t.id === parseInt(req.params.id));
  
  if (index === -1) {
    return ApiResponse.notFound(res, 'Todo not found');
  }
  
  todos.splice(index, 1);
  return ApiResponse.noContent(res);
}));

// 404 handler
app.use((req, res) => {
  return ApiResponse.notFound(res, 'Endpoint not found');
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err);
  return ApiResponse.error(res, err.message || 'Internal server error', err.statusCode || 500);
});

app.listen(3000, () => console.log('API running on port 3000'));

🔑 Key Takeaways

  • Use consistent response format across all endpoints
  • Always include success boolean flag
  • Use appropriate HTTP status codes
  • Create helper functions for common responses
  • Include pagination metadata for lists
  • Provide clear error messages
  • Use 201 for creation, 204 for deletion, 404 for not found

đŸŽ¯ Complete Todo REST API with Express

Here's a complete, production-ready Todo API built with Express:

const express = require('express');
const app = express();

// Middleware
app.use(express.json()); // Parse JSON bodies

// In-memory database (use a real DB in production)
let todos = [
  { id: 1, title: 'Learn Node.js', completed: false },
  { id: 2, title: 'Build an API', completed: false }
];
let nextId = 3;

// GET /todos - Get all todos
app.get('/todos', (req, res) => {
  res.json(todos);
});

// GET /todos/:id - Get one todo
app.get('/todos/:id', (req, res) => {
  const todo = todos.find(t => t.id === parseInt(req.params.id));
  if (!todo) {
    return res.status(404).json({ error: 'Todo not found' });
  }
  res.json(todo);
});

// POST /todos - Create a todo
app.post('/todos', (req, res) => {
  const { title } = req.body;
  
  if (!title) {
    return res.status(400).json({ error: 'Title is required' });
  }
  
  const newTodo = {
    id: nextId++,
    title,
    completed: false
  };
  
  todos.push(newTodo);
  res.status(201).json(newTodo);
});

// PUT /todos/:id - Update a todo
app.put('/todos/:id', (req, res) => {
  const todo = todos.find(t => t.id === parseInt(req.params.id));
  
  if (!todo) {
    return res.status(404).json({ error: 'Todo not found' });
  }
  
  const { title, completed } = req.body;
  if (title !== undefined) todo.title = title;
  if (completed !== undefined) todo.completed = completed;
  
  res.json(todo);
});

// DELETE /todos/:id - Delete a todo
app.delete('/todos/:id', (req, res) => {
  const index = todos.findIndex(t => t.id === parseInt(req.params.id));
  
  if (index === -1) {
    return res.status(404).json({ error: 'Todo not found' });
  }
  
  const deleted = todos.splice(index, 1)[0];
  res.json({ message: 'Todo deleted', todo: deleted });
});

// Start server
app.listen(3000, () => {
  console.log('Todo API running on http://localhost:3000');
});

✅ Express Best Practices

🔐
Environment Variables

Store sensitive data (API keys, DB passwords) in .env files, not in code.

npm install dotenv
require('dotenv').config();
📁
Organize Routes

Split routes into separate files for better organization. Use express.Router().

// routes/users.js
const router = express.Router();
module.exports = router;
âš ī¸
Error Handling Middleware

Create a global error handler to catch all errors in one place.

app.use((err, req, res, next) => {
  res.status(500).json({error: err.message});
});
⚡
Use async/await

For database operations and async code, use async/await for cleaner code.

app.get('/users', async (req, res) => {
  const users = await db.getUsers();
  res.json(users);
});
🎓

You're Now a Full-Stack Developer!

You know JavaScript (browser), Node.js (server), and Express (web framework).
You can build complete applications from frontend to backend!
The journey doesn't stop here — keep building, keep learning! 🚀

What You Can Build

Now that you know Node.js, the possibilities are endless! Here are some of the most popular things you can build:

đŸ–Ĩī¸
Desktop Applications

Build cross-platform desktop apps using Electron or Tauri.

Examples:
â€ĸ VS Code
â€ĸ Slack
â€ĸ Discord
â€ĸ Figma
â€ĸ Notion
Framework: Electron, Tauri
🌐
Web Servers & REST APIs

Build backend servers, REST APIs, GraphQL servers, and real-time applications.

Use cases:
â€ĸ RESTful APIs
â€ĸ GraphQL endpoints
â€ĸ WebSocket servers
â€ĸ Microservices
Framework: Express, Fastify, Nest.js
🤖
Bots & Automation

Create chatbots, automation scripts, web scrapers, and scheduled tasks.

Examples:
â€ĸ Discord bots
â€ĸ Telegram bots
â€ĸ Web scrapers
â€ĸ Task schedulers
Libraries: discord.js, telegraf, puppeteer
đŸ’ģ
Command-Line Tools

Build powerful CLI applications and developer tools.

Examples:
â€ĸ create-react-app
â€ĸ webpack
â€ĸ ESLint
â€ĸ npm/yarn
Libraries: commander, inquirer, chalk
⚡
Real-time Applications

Build live chat apps, multiplayer games, and collaborative tools.

Use cases:
â€ĸ Chat applications
â€ĸ Live dashboards
â€ĸ Multiplayer games
â€ĸ Collaborative editors
Technology: Socket.IO, WebRTC
🔌
IoT & Hardware

Control hardware devices, sensors, and build IoT applications.

Examples:
â€ĸ Raspberry Pi projects
â€ĸ Arduino control
â€ĸ Smart home devices
â€ĸ Robotics
Framework: Johnny-Five, node-serialport

đŸŽ¯ Your Next Steps

With the fundamentals you've learned here, you're ready to dive into any of these directions! The JavaScript knowledge from the Refresher, combined with Node.js modules and the module system, gives you everything you need to start building.

1ī¸âƒŖ Start small: Build a CLI tool or file processor
2ī¸âƒŖ Learn a framework: Express.js for web servers, Electron for desktop apps
3ī¸âƒŖ Explore npm: Discover thousands of packages to supercharge your projects
4ī¸âƒŖ Build projects: The best way to learn is by doing!
🎉

Congratulations!

You now know the fundamentals of Node.js!
JavaScript is no longer just for browsers — you can build anything.
The possibilities are endless. Now go create something amazing! 🚀

📝

Ready to Practice?

Test your knowledge with practice exercises and prepare for your exam.

Go to Exercises →
? Keyboard Shortcuts

âŒ¨ī¸ Keyboard Shortcuts

Next section J
Previous section K
Change language L
Toggle theme T
Scroll to top Home
Close modal Esc