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:
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:
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')?
Resolve the Path: Node.js figures out the full path to math.js
Check Cache: Has this file been loaded before? If yes, return the cached result (skip to step 5)
Load & Wrap: Read the file and wrap it in a function with special variables (module, exports, require, __dirname, __filename)
Execute: Run the entire file from top to bottom
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!
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?
Static Analysis: Before execution, Node.js scans all import statements (they must be at the top!)
Resolve Dependencies: Build a dependency graph of all modules
Load Modules: Fetch all module files in parallel
Parse & Link: Parse each module and link imports to exports
Execute: Run modules in the correct order (dependencies first)
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!
Performance Win: Dynamic imports enable code splitting â load only what you need, when you need it!
âī¸
CommonJS vs ES Modules
Understanding the differences
Feature
CommonJSrequire()
ES Modulesimport
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!
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) => { ... });
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());
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:
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:
Get the requested URL (e.g., /styles/main.css)
Map it to a file path (e.g., ./public/styles/main.css)
Use fs.readFile() to read it
Detect file type and set correct Content-Type
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
Create a folder called public/ in your project
Add HTML, CSS, JS files to it (and subfolders)
Run the server: node server.js
Visit http://localhost:3000
đ Key Concepts Used
path.join() - Safely combine paths (handles OS differences)
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:
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!
# 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.
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!
// 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:
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 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!
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:
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.
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!
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!
// â 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
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)
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
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.
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
}
}
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.