Mastering Express.js: The Complete Handbook from Fundamentals to Data Handling
Unlock the full potential of the industry-standard Node.js framework. This deep dive moves beyond 'Hello World' to cover the entire application lifecycle—including advanced routing strategies, middleware execution order, template engine integration, and handling complex form data with Multer and validation libraries.
Fundamentals
Node.js Prerequisites
Express.js requires Node.js runtime (v14+) with npm/yarn package manager, understanding of JavaScript ES6+ features (arrow functions, promises, async/await), and familiarity with CommonJS (require) or ES modules (import).
# Check prerequisites node --version # Should be v14+ npm --version # Comes with Node.js
Installing Express.js
Express is installed via npm as a project dependency; initialize a project first, then add Express to your package.json.
mkdir my-app && cd my-app npm init -y npm install express
my-app/
├── node_modules/
├── package.json
├── package-lock.json
└── index.js
Creating First Express Application
The minimal Express app creates a server instance, defines a route handler, and listens on a specified port for incoming HTTP connections.
const express = require('express'); const app = express(); app.get('/', (req, res) => { res.send('Hello World!'); }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); });
Understanding app Object
The app object is the central Express instance that holds configuration, routes, middleware, and provides methods for HTTP routing, template engine setup, and server behavior customization.
const app = express(); // Key methods and properties app.set('view engine', 'ejs'); // Configuration app.get() // Route handlers app.use() // Middleware app.listen() // Start server app.locals // App-wide variables
┌─────────────────────────────────────┐
│ app Object │
├─────────────────────────────────────┤
│ .get() .post() .put() .delete() │ ← Routing
│ .use() │ ← Middleware
│ .set() .get() │ ← Config
│ .listen() │ ← Server
│ .locals │ ← Variables
└─────────────────────────────────────┘
Basic Routing (GET, POST, PUT, DELETE)
Routes define how the application responds to client requests at specific endpoints; each HTTP method has a corresponding Express method that takes a path and handler function.
// CREATE app.post('/users', (req, res) => { res.status(201).json({ message: 'User created' }); }); // READ app.get('/users', (req, res) => { res.json([{ id: 1, name: 'John' }]); }); // UPDATE app.put('/users/:id', (req, res) => { res.json({ message: `User ${req.params.id} updated` }); }); // DELETE app.delete('/users/:id', (req, res) => { res.status(204).send(); });
HTTP Method │ CRUD Operation │ Express Method
────────────┼────────────────┼───────────────
GET │ Read │ app.get()
POST │ Create │ app.post()
PUT/PATCH │ Update │ app.put()
DELETE │ Delete │ app.delete()
Route Parameters
Route parameters are named URL segments prefixed with colon (:) that capture dynamic values and expose them in req.params object for use in handlers.
// Single parameter app.get('/users/:id', (req, res) => { res.send(`User ID: ${req.params.id}`); }); // Multiple parameters app.get('/users/:userId/posts/:postId', (req, res) => { const { userId, postId } = req.params; res.json({ userId, postId }); }); // Optional parameter (with regex) app.get('/files/:name.:ext?', (req, res) => { res.json(req.params); // { name: 'doc', ext: 'pdf' } });
URL: /users/42/posts/7
Pattern: /users/:userId/posts/:postId
│ │
▼ ▼
req.params = { userId: "42", postId: "7" }
Query Strings
Query strings are key-value pairs appended to URLs after ? and are automatically parsed by Express into the req.query object for filtering, pagination, or passing optional data.
// URL: /search?q=express&limit=10&page=2 app.get('/search', (req, res) => { const { q, limit = 20, page = 1 } = req.query; res.json({ searchTerm: q, // 'express' limit: parseInt(limit), // 10 page: parseInt(page) // 2 }); });
URL: /products?category=electronics&sort=price&order=desc
│ │ │
▼ ▼ ▼
req.query = { category: "electronics", sort: "price", order: "desc" }
Request Object (req)
The req object represents the HTTP request with properties for query strings, parameters, body content, headers, cookies, and methods to inspect the incoming request.
app.post('/api/data', (req, res) => { console.log(req.method); // 'POST' console.log(req.path); // '/api/data' console.log(req.params); // Route parameters console.log(req.query); // Query string console.log(req.body); // Request body (parsed) console.log(req.headers); // HTTP headers console.log(req.cookies); // Cookies (with parser) console.log(req.ip); // Client IP console.log(req.get('Content-Type')); // Header value });
┌──────────────────────────────────────────┐
│ req Object │
├──────────────────────────────────────────┤
│ .params → URL parameters (:id) │
│ .query → Query string (?key=val) │
│ .body → POST/PUT data │
│ .headers → HTTP headers │
│ .method → GET, POST, etc. │
│ .path → URL path │
│ .ip → Client IP address │
└──────────────────────────────────────────┘
Response Object (res)
The res object represents the HTTP response with methods to send data, set status codes, headers, cookies, and control how data is returned to the client.
app.get('/example', (req, res) => { // Status codes res.status(200); // Set headers res.set('X-Custom-Header', 'value'); // Set cookie res.cookie('session', 'abc123', { httpOnly: true }); // Chainable res.status(201).json({ created: true }); }); // Redirect app.get('/old', (req, res) => res.redirect('/new'));
┌──────────────────────────────────────────┐
│ res Object │
├──────────────────────────────────────────┤
│ .send() → Send string/buffer │
│ .json() → Send JSON │
│ .status() → Set HTTP status │
│ .redirect() → Redirect to URL │
│ .render() → Render template │
│ .sendFile() → Send file │
│ .set() → Set header │
│ .cookie() → Set cookie │
└──────────────────────────────────────────┘
Sending Responses (JSON, HTML, Files)
Express provides multiple methods to send different response types; send() for auto-detected content, json() for APIs, sendFile() for files, and render() for templates.
// JSON response (most common for APIs) app.get('/api/users', (req, res) => { res.json({ users: [{ id: 1, name: 'John' }] }); }); // HTML response app.get('/page', (req, res) => { res.send('<h1>Hello HTML</h1>'); }); // File download app.get('/download', (req, res) => { res.download('./files/report.pdf'); }); // Send file inline app.get('/image', (req, res) => { res.sendFile(__dirname + '/images/logo.png'); }); // Render template app.get('/home', (req, res) => { res.render('index', { title: 'Home' }); });
Static File Serving
The express.static() middleware serves static assets (CSS, JavaScript, images) from a specified directory, making them accessible via URL paths without explicit route handlers.
const path = require('path'); // Serve from 'public' folder app.use(express.static('public')); // With virtual path prefix app.use('/assets', express.static('public')); // With absolute path (recommended) app.use(express.static(path.join(__dirname, 'public')));
Project Structure: URL Access:
───────────────── ───────────
public/
├── css/
│ └── style.css → /css/style.css
├── js/
│ └── app.js → /js/app.js
└── images/
└── logo.png → /images/logo.png
Express Generator
Express Generator is an official CLI tool that scaffolds a complete Express application structure with sensible defaults, view engines, and common middleware pre-configured.
# Install globally npm install -g express-generator # Generate app with options express --view=ejs myapp # Or use npx (no install) npx express-generator --view=ejs myapp cd myapp && npm install && npm start
myapp/
├── bin/
│ └── www ← Server entry point
├── public/
│ ├── images/
│ ├── javascripts/
│ └── stylesheets/
├── routes/
│ ├── index.js
│ └── users.js
├── views/
│ ├── error.ejs
│ └── index.ejs
├── app.js ← Main application
└── package.json
Core Concepts
Middleware Concept
Middleware functions are the backbone of Express—they have access to req, res, and next(), executing sequentially to process requests, modify objects, end cycles, or pass control to the next function.
// Middleware signature const myMiddleware = (req, res, next) => { // 1. Execute code console.log('Request received'); // 2. Modify req/res req.requestTime = Date.now(); // 3. End cycle OR call next() next(); // Pass to next middleware }; app.use(myMiddleware);
Request → [Middleware 1] → [Middleware 2] → [Route Handler] → Response
│ │ │
▼ ▼ ▼
Logging Auth Check Send Data
Built-in Middleware
Express 4.x includes three built-in middleware functions: express.json() for parsing JSON bodies, express.urlencoded() for form data, and express.static() for serving static files.
// Parse JSON bodies app.use(express.json()); // Parse URL-encoded bodies (forms) app.use(express.urlencoded({ extended: true })); // Serve static files app.use(express.static('public')); // With options app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' }));
┌─────────────────────────────────────────────────┐
│ Express Built-in Middleware │
├─────────────────────────────────────────────────┤
│ express.json() → Parse JSON bodies │
│ express.urlencoded() → Parse form data │
│ express.static() → Serve static files │
│ express.raw() → Parse raw buffers │
│ express.text() → Parse text bodies │
└─────────────────────────────────────────────────┘
Application-level Middleware
Application-level middleware binds to the app object using app.use() or app.METHOD(), applying to all routes or specific paths across the entire application.
// Applies to ALL requests app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); next(); }); // Applies to specific path app.use('/api', (req, res, next) => { req.apiVersion = 'v1'; next(); }); // Applies to specific method + path app.get('/admin', authMiddleware, (req, res) => { res.send('Admin panel'); });
Router-level Middleware
Router-level middleware works identically to application-level but binds to an express.Router() instance, allowing modular middleware scoping for route groups.
const router = express.Router(); // Middleware for all routes in this router router.use((req, res, next) => { console.log('Router-level middleware'); next(); }); // Middleware for specific route router.use('/profile', requireAuth); router.get('/profile', (req, res) => { res.json({ user: req.user }); }); // Mount router app.use('/users', router);
app.use('/users', router)
│
▼
┌─────────┐
│ Router │
├─────────┤
│ .use() │ ← Router-level middleware
│ .get() │
│ .post() │
└─────────┘
Error-handling Middleware
Error-handling middleware has four parameters (err, req, res, next) and catches errors thrown or passed via next(err), defined after all other routes and middleware.
// Route that throws error app.get('/error', (req, res, next) => { const err = new Error('Something went wrong'); err.status = 500; next(err); // Pass to error handler }); // Error-handling middleware (MUST have 4 params) app.use((err, req, res, next) => { console.error(err.stack); res.status(err.status || 500).json({ error: { message: err.message, status: err.status } }); });
Normal Flow: req → middleware → route → res
│
Error Flow: req → middleware → route ─┐
▼ next(err)
┌─────────────────────┐
│ Error Handler │
│ (err, req, res, next)│
└─────────────────────┘
Third-party Middleware
Third-party middleware extends Express functionality for common tasks like logging, security, compression, and session management; installed via npm and integrated with app.use().
const morgan = require('morgan'); // Logging const helmet = require('helmet'); // Security headers const cors = require('cors'); // CORS handling const compression = require('compression'); // Apply third-party middleware app.use(helmet()); // Security first app.use(cors()); // Enable CORS app.use(compression()); // Gzip compression app.use(morgan('dev')); // Request logging
Popular Third-party Middleware:
───────────────────────────────
morgan → HTTP request logging
helmet → Security headers
cors → Cross-Origin Resource Sharing
compression → Gzip compression
cookie-parser → Parse cookies
express-session → Session management
express-validator → Input validation
multer → File uploads
Middleware Execution Order
Middleware executes in the exact order defined in code; order matters critically for functionality—authentication before routes, error handlers last, parsers before body access.
// ✅ CORRECT ORDER app.use(helmet()); // 1. Security app.use(cors()); // 2. CORS app.use(morgan('dev')); // 3. Logging app.use(express.json()); // 4. Body parsing app.use('/api', authMiddleware); // 5. Authentication app.use('/api', apiRoutes); // 6. Routes app.use(notFoundHandler); // 7. 404 handler app.use(errorHandler); // 8. Error handler (LAST)
Request
│
▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐
│ helmet() │ → │ cors() │ → │ morgan() │ → │ json()│
└──────────┘ └──────────┘ └──────────┘ └───────┘
│
┌─────────────────────────────────────────────┘
▼
┌──────────┐ ┌──────────┐ ┌───────────────┐
│ Auth │ → │ Routes │ → │ Error Handler │
└──────────┘ └──────────┘ └───────────────┘
│
▼
Response
next() Function
The next() function passes control to the next middleware; calling next() with no argument continues the chain, while next(err) skips to error handlers, and next('route') skips remaining callbacks.
// Normal next - continue chain app.use((req, res, next) => { req.timestamp = Date.now(); next(); // Continue to next middleware }); // Error next - jump to error handler app.use((req, res, next) => { if (!req.headers.authorization) { return next(new Error('Unauthorized')); } next(); }); // Skip to next route app.get('/user/:id', (req, res, next) => { if (req.params.id === '0') next('route'); // Skip remaining else next(); }, (req, res) => { res.send('Regular user'); } ); app.get('/user/:id', (req, res) => { res.send('Special user 0'); });
next() → Continue to next middleware
next(err) → Skip to error handler
next('route') → Skip to next route match
(no next) → End request (must send response)
Express Router
Express Router is a mini-application capable of performing middleware and routing, allowing you to create modular, mountable route handlers as separate files for better code organization.
// routes/users.js const express = require('express'); const router = express.Router(); router.get('/', (req, res) => { res.json({ users: [] }); }); router.get('/:id', (req, res) => { res.json({ user: { id: req.params.id } }); }); router.post('/', (req, res) => { res.status(201).json({ created: true }); }); module.exports = router; // app.js const userRoutes = require('./routes/users'); app.use('/api/users', userRoutes);
Route Modularization
Route modularization separates routes into domain-specific files, each exporting a router instance, then mounting them on the main app for maintainable, scalable codebases.
project/
├── app.js
├── routes/
│ ├── index.js ← Route aggregator
│ ├── auth.js ← /auth/*
│ ├── users.js ← /users/*
│ └── products.js ← /products/*
// routes/index.js const express = require('express'); const router = express.Router(); router.use('/auth', require('./auth')); router.use('/users', require('./users')); router.use('/products', require('./products')); module.exports = router; // app.js app.use('/api', require('./routes')); // Results in: /api/auth/*, /api/users/*, /api/products/*
Route Grouping
Route grouping organizes related routes under common prefixes or shared middleware, reducing redundancy and centralizing cross-cutting concerns like authentication or validation.
const router = express.Router(); // Group with shared middleware const adminRoutes = express.Router(); adminRoutes.use(requireAdmin); // Applied to all below adminRoutes.get('/dashboard', getDashboard); adminRoutes.get('/users', getAllUsers); adminRoutes.delete('/users/:id', deleteUser); app.use('/admin', adminRoutes); // Alternative: Array of middleware const protectedMiddleware = [authenticate, rateLimit]; app.get('/profile', protectedMiddleware, getProfile); app.put('/profile', protectedMiddleware, updateProfile);
/admin (protected by requireAdmin)
├── GET /dashboard → getDashboard
├── GET /users → getAllUsers
└── DELETE /users/:id → deleteUser
Request Body Parsing
Request body parsing converts incoming request payloads (JSON, form data, raw) into usable JavaScript objects accessible via req.body; requires appropriate middleware before route handlers.
// Enable body parsing (must be before routes) app.use(express.json()); // JSON app.use(express.urlencoded({ extended: true })); // Forms app.post('/api/data', (req, res) => { console.log(req.body); // Parsed body available res.json({ received: req.body }); });
Client Request req.body
───────────── ─────────
Content-Type: application/json
{"name":"John"} → { name: "John" }
Content-Type: application/x-www-form-urlencoded
name=John&age=30 → { name: "John", age: "30" }
URL-encoded Data Handling
URL-encoded data comes from HTML forms with application/x-www-form-urlencoded content type; express.urlencoded() parses this into req.body with extended: true allowing nested objects.
// extended: true → Uses 'qs' library (nested objects) // extended: false → Uses 'querystring' (flat) app.use(express.urlencoded({ extended: true })); app.post('/form', (req, res) => { // Form: name=John&address[city]=NYC&address[zip]=10001 console.log(req.body); // { name: 'John', address: { city: 'NYC', zip: '10001' } } });
<form method="POST" action="/form"> <input name="username" value="john"> <input name="profile[age]" value="25"> <button type="submit">Submit</button> </form>
JSON Body Parsing
The express.json() middleware parses incoming requests with Content-Type: application/json, converting JSON payloads to JavaScript objects in req.body with configurable options for limits and verification.
// Basic usage app.use(express.json()); // With options app.use(express.json({ limit: '10mb', // Max body size strict: true, // Only objects/arrays type: 'application/json' // Content-Type to parse })); app.post('/api/users', (req, res) => { const { name, email } = req.body; res.status(201).json({ message: 'User created', user: { name, email } }); });
POST /api/users
Content-Type: application/json
{ "name": "John", "email": "john@example.com" }
│
▼ express.json()
req.body = { name: "John", email: "john@example.com" }
Templating & Views
Template Engines Overview
Template engines allow you to use static template files in your application, replacing variables with actual values at runtime and transforming the template into HTML sent to the client. Express supports multiple engines like EJS, Pug, and Handlebars through a consistent res.render() API.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Template File │ ──► │ Template Engine │ ──► │ HTML Output │
│ (+ Data) │ │ (EJS/Pug/HBS) │ │ (to client) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
app.set('view engine', 'ejs'); // Set default engine app.set('views', './views'); // Set views directory
EJS Integration
EJS (Embedded JavaScript) uses plain JavaScript syntax with <%= %> for output and <% %> for logic, making it easy for developers already familiar with JavaScript and HTML to adopt without learning new syntax.
npm install ejs
// app.js app.set('view engine', 'ejs'); // views/user.ejs <h1>Welcome, <%= user.name %></h1> <% if (user.isAdmin) { %> <span class="badge">Admin</span> <% } %> <ul> <% items.forEach(item => { %> <li><%= item %></li> <% }); %> </ul>
Pug/Jade Integration
Pug (formerly Jade) uses indentation-based syntax without closing tags, resulting in cleaner and more concise templates but requiring developers to learn its unique whitespace-sensitive syntax.
npm install pug
// app.js app.set('view engine', 'pug'); // views/user.pug doctype html html head title= title body h1 Welcome, #{user.name} if user.isAdmin span.badge Admin ul each item in items li= item
Handlebars Integration
Handlebars provides logic-less templates with a mustache-style syntax {{}}, enforcing separation of concerns by limiting logic in templates and using helpers for complex operations.
npm install express-handlebars
const exphbs = require('express-handlebars'); app.engine('hbs', exphbs.engine({ extname: '.hbs' })); app.set('view engine', 'hbs'); // views/user.hbs <h1>Welcome, {{user.name}}</h1> {{#if user.isAdmin}} <span class="badge">Admin</span> {{/if}} {{#each items}} <li>{{this}}</li> {{/each}}
View Rendering
The res.render() method compiles a template with provided data and sends the resulting HTML response, automatically setting the correct content-type header and handling errors.
app.get('/profile/:id', async (req, res) => { try { const user = await User.findById(req.params.id); res.render('profile', { user, title: 'Profile Page' }); } catch (err) { res.render('error', { message: 'User not found' }); } }); // With callback for custom handling res.render('template', { data }, (err, html) => { if (err) return res.status(500).send('Render failed'); res.send(html.toUpperCase()); // Modify before sending });
Passing Data to Views
Data is passed to templates as the second argument to res.render(), and res.locals provides a way to set variables accessible to all templates rendered during a request, while app.locals sets app-wide variables.
// App-wide data (available in ALL templates) app.locals.siteName = 'MyApp'; app.locals.year = new Date().getFullYear(); // Request-specific data via middleware app.use((req, res, next) => { res.locals.user = req.user; // Current user res.locals.flash = req.flash(); // Flash messages next(); }); // Route-specific data app.get('/dashboard', (req, res) => { res.render('dashboard', { stats: { visits: 1000 }, // Merged with res.locals pageTitle: 'Dashboard' }); });
┌─────────────────────────────────────────────────┐
│ Template Data Hierarchy │
├─────────────────────────────────────────────────┤
│ 1. app.locals (global, all requests) │
│ 2. res.locals (per-request) │
│ 3. render() data (per-render, highest priority)│
└─────────────────────────────────────────────────┘
Layouts and Partials
Layouts define the common structure (header, footer, nav) of pages while partials are reusable template fragments; implementation varies by engine, with some requiring additional packages like express-ejs-layouts.
// Using express-ejs-layouts const expressLayouts = require('express-ejs-layouts'); app.use(expressLayouts); app.set('layout', 'layouts/main'); // views/layouts/main.ejs <!DOCTYPE html> <html> <head><title><%= title %></title></head> <body> <%- include('../partials/header') %> <%- body %> <!-- Page content injected here --> <%- include('../partials/footer') %> </body> </html> // views/partials/header.ejs <nav>Welcome, <%= user?.name || 'Guest' %></nav>
┌──────────────────────────────────────┐
│ Layout (main) │
│ ┌────────────────────────────────┐ │
│ │ Partial (header) │ │
│ └────────────────────────────────┘ │
│ ┌────────────────────────────────┐ │
│ │ Page Content (body) │ │
│ └────────────────────────────────┘ │
│ ┌────────────────────────────────┐ │
│ │ Partial (footer) │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
View Caching
In production, Express caches compiled templates in memory to avoid re-reading and re-compiling template files on each request, significantly improving performance; this is enabled by default when NODE_ENV=production.
// Automatically enabled in production // NODE_ENV=production node app.js // Manual control app.set('view cache', true); // Enable caching app.set('view cache', false); // Disable (for development) // Check current environment app.get('env'); // Returns 'development' or 'production' // Typical setup if (process.env.NODE_ENV === 'production') { app.set('view cache', true); console.log('Template caching enabled'); }
Advanced Routing
Route Chaining
Route chaining allows you to define multiple HTTP method handlers for the same path using app.route(), reducing redundancy and grouping related operations together for cleaner code organization.
app.route('/articles') .get((req, res) => { res.json({ action: 'List all articles' }); }) .post((req, res) => { res.json({ action: 'Create article' }); }) .delete((req, res) => { res.json({ action: 'Delete all articles' }); }); app.route('/articles/:id') .get((req, res) => res.json({ action: `Get article ${req.params.id}` })) .put((req, res) => res.json({ action: `Update article ${req.params.id}` })) .delete((req, res) => res.json({ action: `Delete article ${req.params.id}` }));
Route Handlers Arrays
Multiple handler functions can be passed as an array or as successive arguments, enabling modular middleware composition where each handler can process the request, modify it, or short-circuit the chain.
const authenticate = (req, res, next) => { if (!req.headers.authorization) return res.status(401).json({ error: 'Unauthorized' }); next(); }; const authorize = (role) => (req, res, next) => { if (req.user.role !== role) return res.status(403).json({ error: 'Forbidden' }); next(); }; const validate = (req, res, next) => { if (!req.body.title) return res.status(400).json({ error: 'Title required' }); next(); }; const createPost = (req, res) => res.status(201).json({ created: true }); // As array app.post('/posts', [authenticate, authorize('admin'), validate, createPost]); // As arguments app.post('/posts', authenticate, authorize('admin'), validate, createPost);
Request ──► authenticate ──► authorize ──► validate ──► createPost ──► Response
│ │ │
▼ ▼ ▼
401 403 400
(if fail) (if fail) (if fail)
Regular Expressions in Routes
Express supports regex patterns in route paths for flexible matching, allowing complex URL patterns like matching specific formats, optional segments, or character classes.
// Match anything ending with 'fly' (butterfly, dragonfly) app.get(/.*fly$/, (req, res) => { res.send(`Matched: ${req.path}`); }); // Match /user/123 or /user/abc (alphanumeric IDs) app.get(/^\/user\/([a-zA-Z0-9]+)$/, (req, res) => { res.send(`User ID: ${req.params[0]}`); }); // Match routes with optional 's' (/book or /books) app.get('/books?', (req, res) => res.send('Book(s) list')); // Match /ab, /aab, /aaab, etc. app.get('/a+b', (req, res) => res.send('Matched a+b')); // Match /abcd, /abxcd, /ab123cd, etc. app.get('/ab*cd', (req, res) => res.send('Matched ab*cd'));
Route Parameter Validation
The app.param() method allows you to add validation/preprocessing logic that runs automatically whenever a specific route parameter is present, centralizing parameter handling across multiple routes.
// Validate and preload user for any route with :userId app.param('userId', async (req, res, next, id) => { // Validate format if (!/^\d+$/.test(id)) { return res.status(400).json({ error: 'Invalid user ID format' }); } try { const user = await User.findById(id); if (!user) return res.status(404).json({ error: 'User not found' }); req.user = user; // Attach to request next(); } catch (err) { next(err); } }); // All these routes now have req.user available app.get('/users/:userId', (req, res) => res.json(req.user)); app.get('/users/:userId/posts', (req, res) => res.json(req.user.posts)); app.put('/users/:userId', (req, res) => { /* update req.user */ });
app.route()
The app.route() method returns a chainable route instance for a single path, allowing you to attach handlers for different HTTP methods while avoiding path duplication and typos.
// Without app.route() - repetitive path app.get('/books', getBooks); app.post('/books', createBook); app.delete('/books', deleteBooks); // With app.route() - DRY principle app.route('/books') .all((req, res, next) => { console.log(`${req.method} /books`); next(); }) .get(getBooks) .post(createBook) .delete(deleteBooks); // Complex example with middleware app.route('/admin/users/:id') .all(authenticate, authorizeAdmin) .get(getUser) .put(validateUserUpdate, updateUser) .delete(deleteUser);
express.Router() Advanced Patterns
Express Router is a mini-application capable of performing middleware and routing functions; advanced patterns include configurable routers, router-level middleware, and factory functions for creating route variations.
// Configurable router factory const createCrudRouter = (model) => { const router = express.Router({ mergeParams: true }); router.route('/') .get(async (req, res) => res.json(await model.find())) .post(async (req, res) => res.json(await model.create(req.body))); router.route('/:id') .get(async (req, res) => res.json(await model.findById(req.params.id))) .put(async (req, res) => res.json(await model.update(req.params.id, req.body))) .delete(async (req, res) => res.json(await model.delete(req.params.id))); return router; }; // Router options const router = express.Router({ caseSensitive: true, // /Foo !== /foo mergeParams: true, // Inherit parent params strict: true // /foo !== /foo/ }); app.use('/api/users', createCrudRouter(UserModel)); app.use('/api/posts', createCrudRouter(PostModel));
Nested Routers
Routers can be mounted within other routers to create hierarchical route structures, enabling organization of complex APIs into modular, maintainable pieces with inherited parameters.
// routes/users.js const userRouter = express.Router(); const postRouter = express.Router({ mergeParams: true }); // Access parent params // Nested: /users/:userId/posts postRouter.get('/', (req, res) => { res.json({ userId: req.params.userId, action: 'list posts' }); }); postRouter.post('/', (req, res) => { res.json({ userId: req.params.userId, action: 'create post' }); }); // Parent router userRouter.get('/', (req, res) => res.json({ action: 'list users' })); userRouter.get('/:userId', (req, res) => res.json({ userId: req.params.userId })); userRouter.use('/:userId/posts', postRouter); // Mount nested router // app.js app.use('/users', userRouter);
/users
├── GET / → list users
├── GET /:userId → get user
└── /users/:userId/posts
├── GET / → list user's posts
└── POST / → create user's post
Dynamic Route Loading
Routes can be loaded dynamically at runtime by scanning a directory and requiring route files automatically, reducing boilerplate and enabling modular file organization.
const fs = require('fs'); const path = require('path'); // Automatically load all route files from 'routes' directory const routesPath = path.join(__dirname, 'routes'); fs.readdirSync(routesPath) .filter(file => file.endsWith('.js')) .forEach(file => { const route = require(path.join(routesPath, file)); const routeName = file.replace('.js', ''); app.use(`/api/${routeName}`, route); console.log(`Loaded route: /api/${routeName}`); }); // routes/users.js const router = express.Router(); router.get('/', (req, res) => res.json({ route: 'users' })); module.exports = router; // Output: // Loaded route: /api/users // Loaded route: /api/posts // Loaded route: /api/comments
project/
├── app.js
└── routes/
├── users.js → /api/users
├── posts.js → /api/posts
└── comments.js → /api/comments
Route Prefixing
Route prefixing is achieved by mounting routers at specific paths, allowing you to version APIs, separate admin routes, or organize routes by feature with a common base path.
const v1Router = express.Router(); const v2Router = express.Router(); const adminRouter = express.Router(); // V1 API v1Router.get('/users', (req, res) => res.json({ version: 1, data: [] })); // V2 API with breaking changes v2Router.get('/users', (req, res) => res.json({ version: 2, users: [], meta: {} })); // Admin routes with auth adminRouter.use(authenticate, authorizeAdmin); adminRouter.get('/stats', (req, res) => res.json({ stats: {} })); adminRouter.get('/users', (req, res) => res.json({ allUsers: [] })); // Mount with prefixes app.use('/api/v1', v1Router); // /api/v1/users app.use('/api/v2', v2Router); // /api/v2/users app.use('/admin', adminRouter); // /admin/stats // Multiple prefixes for same router app.use(['/api', '/rest'], apiRouter);
┌─────────────────────────────────────┐
│ Express App │
├─────────────────────────────────────┤
│ /api/v1/* → v1Router │
│ /api/v2/* → v2Router │
│ /admin/* → adminRouter (auth) │
│ /public/* → publicRouter │
└─────────────────────────────────────┘
Data Handling
Form Handling
Express handles HTML form submissions using express.urlencoded() middleware for application/x-www-form-urlencoded content type (default for HTML forms), parsing form fields into req.body.
// Enable form parsing app.use(express.urlencoded({ extended: true })); // Parse form data app.use(express.json()); // Parse JSON app.post('/register', (req, res) => { const { username, email, password } = req.body; console.log('Form data:', req.body); // { username: 'john', email: 'john@example.com', password: '123' } res.redirect('/login'); }); // HTML form // <form action="/register" method="POST"> // <input name="username" type="text"> // <input name="email" type="email"> // <input name="password" type="password"> // <button type="submit">Register</button> // </form>
File Uploads (Multer)
Multer is a middleware for handling multipart/form-data, primarily used for file uploads; it adds a file or files object to the request containing uploaded file information.
npm install multer
const multer = require('multer'); // Disk storage configuration const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, 'uploads/'), filename: (req, file, cb) => { const uniqueName = `${Date.now()}-${file.originalname}`; cb(null, uniqueName); } }); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, // 5MB fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) cb(null, true); else cb(new Error('Only images allowed'), false); } }); // Single file app.post('/avatar', upload.single('avatar'), (req, res) => { console.log(req.file); // { filename, path, size, mimetype... } res.json({ path: req.file.path }); }); // Multiple files app.post('/gallery', upload.array('photos', 10), (req, res) => { console.log(req.files); // Array of file objects res.json({ count: req.files.length }); });
Multipart Form Data
Multipart form data allows sending both files and text fields in a single request; Multer handles this by populating req.body with text fields and req.file/req.files with uploaded files.
const upload = multer({ dest: 'uploads/' }); // Mixed: text fields + files app.post('/product', upload.fields([ { name: 'image', maxCount: 1 }, { name: 'gallery', maxCount: 5 } ]), (req, res) => { console.log(req.body); // { name: 'Product', price: '99.99' } console.log(req.files); // { image: [...], gallery: [...] } res.json({ success: true }); }); // Text-only multipart (no file upload, just parsing) app.post('/form', upload.none(), (req, res) => { res.json(req.body); });
Content-Type: multipart/form-data; boundary=----FormBoundary
------FormBoundary
Content-Disposition: form-data; name="name"
Product Name
------FormBoundary
Content-Disposition: form-data; name="image"; filename="photo.jpg"
Content-Type: image/jpeg
[binary data]
------FormBoundary--
Cookies (cookie-parser)
The cookie-parser middleware parses Cookie headers and populates req.cookies with an object keyed by cookie names, optionally supporting signed cookies for data integrity verification.
npm install cookie-parser
const cookieParser = require('cookie-parser'); app.use(cookieParser('my-secret-key')); // Secret for signed cookies // Set cookies app.get('/set-cookie', (req, res) => { // Simple cookie res.cookie('theme', 'dark', { maxAge: 86400000 }); // 1 day // Secure cookie with options res.cookie('token', 'abc123', { httpOnly: true, // Not accessible via JavaScript secure: true, // HTTPS only sameSite: 'strict', // CSRF protection maxAge: 3600000 // 1 hour }); // Signed cookie (tamper-proof) res.cookie('userId', '42', { signed: true }); res.send('Cookies set'); }); // Read cookies app.get('/read-cookie', (req, res) => { console.log(req.cookies); // { theme: 'dark', token: 'abc123' } console.log(req.signedCookies); // { userId: '42' } res.json(req.cookies); }); // Delete cookie app.get('/logout', (req, res) => { res.clearCookie('token'); res.send('Logged out'); });
Sessions (express-session)
Sessions store user data server-side between requests, using a cookie to track the session ID; express-session supports various stores like memory (development), Redis, or MongoDB for production scalability.
npm install express-session connect-redis redis
const session = require('express-session'); const RedisStore = require('connect-redis').default; const { createClient } = require('redis'); // Redis client for production const redisClient = createClient(); redisClient.connect(); app.use(session({ store: new RedisStore({ client: redisClient }), // Production store secret: 'keyboard-cat', resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', maxAge: 24 * 60 * 60 * 1000 // 24 hours } })); // Use session app.post('/login', (req, res) => { req.session.userId = user.id; req.session.role = user.role; res.json({ message: 'Logged in' }); }); app.get('/dashboard', (req, res) => { if (!req.session.userId) return res.redirect('/login'); res.json({ userId: req.session.userId }); }); app.post('/logout', (req, res) => { req.session.destroy(err => { res.clearCookie('connect.sid'); res.json({ message: 'Logged out' }); }); });
┌─────────┐ Session ID (cookie) ┌─────────┐
│ Browser │ ◄────────────────────────────► │ Express │
└─────────┘ └────┬────┘
│
┌───────────────────────────┘
▼
┌─────────────────┐
│ Session Store │
│ (Redis/Mongo) │
│ { sid: data } │
└─────────────────┘
Flash Messages
Flash messages are temporary session-based messages that survive one redirect, commonly used to display success/error notifications after form submissions; they are typically implemented with connect-flash.
npm install connect-flash express-session
const flash = require('connect-flash'); app.use(session({ /* config */ })); app.use(flash()); // Make flash messages available to all views app.use((req, res, next) => { res.locals.success = req.flash('success'); res.locals.error = req.flash('error'); next(); }); // Set flash message before redirect app.post('/register', async (req, res) => { try { await User.create(req.body); req.flash('success', 'Registration successful! Please login.'); res.redirect('/login'); } catch (err) { req.flash('error', 'Registration failed: ' + err.message); res.redirect('/register'); } }); // View (EJS) // <% if (success.length) { %> // <div class="alert success"><%= success %></div> // <% } %> // <% if (error.length) { %> // <div class="alert error"><%= error %></div> // <% } %>
POST /register ──► req.flash('success', 'Done!') ──► redirect('/login')
│
GET /login ◄────────────────────────────────────────────────┘
│
└──► res.locals.success = 'Done!' (available in view, then cleared)
Data Validation (express-validator, Joi)
Validation libraries ensure incoming data meets requirements before processing; express-validator integrates as middleware chains while Joi provides schema-based validation with detailed error messages.
npm install express-validator joi
// express-validator approach const { body, validationResult } = require('express-validator'); app.post('/users', body('email').isEmail().normalizeEmail(), body('password').isLength({ min: 8 }).matches(/\d/), body('age').isInt({ min: 18, max: 120 }), (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } res.json({ valid: true }); } ); // Joi approach const Joi = require('joi'); const userSchema = Joi.object({ email: Joi.string().email().required(), password: Joi.string().min(8).pattern(/\d/).required(), age: Joi.number().integer().min(18).max(120) }); const validate = (schema) => (req, res, next) => { const { error, value } = schema.validate(req.body, { abortEarly: false }); if (error) return res.status(400).json({ errors: error.details }); req.body = value; // Use sanitized value next(); }; app.post('/users', validate(userSchema), (req, res) => { res.json({ valid: true }); });
Data Sanitization
Sanitization cleans and transforms input data to prevent XSS attacks, SQL injection, and ensure consistent data formats; it typically runs alongside or after validation using libraries like express-validator or sanitize-html.
npm install express-validator sanitize-html
const { body } = require('express-validator'); const sanitizeHtml = require('sanitize-html'); // Built-in sanitizers in express-validator app.post('/profile', body('email').normalizeEmail(), // lowercase, remove dots from gmail body('website').trim().toLowerCase(), body('name').trim().escape(), // HTML encode special chars body('bio').customSanitizer(value => { return sanitizeHtml(value, { allowedTags: ['b', 'i', 'em', 'strong', 'a'], allowedAttributes: { 'a': ['href'] } }); }), (req, res) => { res.json(req.body); } ); // Common sanitization patterns const sanitize = { noHtml: (str) => str.replace(/<[^>]*>/g, ''), alphanumeric: (str) => str.replace(/[^a-zA-Z0-9]/g, ''), slug: (str) => str.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '') };
Input: "<script>alert('xss')</script>Hello" │ ▼ ┌───────────────┐ │ Sanitizer │ │ (escape/ │ │ strip HTML) │ └───────────────┘ │ ▼ Output: "<script>...Hello" or "Hello"