Express.js is the most widely used Node.js web framework. This cheat sheet covers every pattern you'll reach for — routing, middleware, request parsing, error handling, and building REST APIs — with copy-ready code for each.
Quick reference
The 25 patterns that cover 90% of Express development.
| Pattern | Code |
|---|---|
| Install | npm install express |
| Hello World | app.get('/', (req, res) => res.send('Hello')) |
| JSON response | res.json({ ok: true }) |
| Status + JSON | res.status(201).json(data) |
| Route params | app.get('/users/:id', ...) → req.params.id |
| Query string | GET /search?q=foo → req.query.q |
| Parse JSON body | app.use(express.json()) |
| Parse form body | app.use(express.urlencoded({ extended: true })) |
| Static files | app.use(express.static('public')) |
| Custom middleware | app.use((req, res, next) => { ...; next() }) |
| Router | const r = express.Router(); app.use('/api', r) |
| Error middleware | app.use((err, req, res, next) => ...) |
| Async handler | asyncHandler(async (req, res) => ...) |
| Redirect | res.redirect(301, '/new-path') |
| Send file | res.sendFile(path.resolve('file.pdf')) |
| Set header | res.set('X-Custom', 'value') |
| Get header | req.get('Authorization') |
| Cookie | res.cookie('token', val, { httpOnly: true }) |
| Clear cookie | res.clearCookie('token') |
| 404 handler | app.use((req, res) => res.status(404).json(...)) |
| Listen | app.listen(3000, () => console.log('ready')) |
| Trust proxy | app.set('trust proxy', 1) |
| Disable x-powered-by | app.disable('x-powered-by') |
| Request IP | req.ip |
| Method override | app.use(methodOverride('_method')) |
Installation and setup
npm install express
Minimal server:
import express from 'express';
const app = express();
// Body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => {
res.json({ message: 'Hello, world!' });
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Use "type": "module" in package.json for ESM import syntax, or use require('express') for CommonJS.
Routing
Basic routes
app.get('/users', (req, res) => { /* list users */ });
app.post('/users', (req, res) => { /* create user */ });
app.put('/users/:id', (req, res) => { /* replace user */ });
app.patch('/users/:id', (req, res) => { /* update fields */ });
app.delete('/users/:id', (req, res) => { /* delete user */ });
Match all HTTP methods: app.all('/path', handler).
Route parameters
app.get('/users/:id', (req, res) => {
const { id } = req.params; // string — cast if needed
res.json({ userId: id });
});
// Multiple params
app.get('/posts/:year/:slug', (req, res) => {
const { year, slug } = req.params;
// ...
});
// Optional param with regex
app.get('/files/:name.:ext?', (req, res) => {
// matches /files/photo and /files/photo.jpg
});
Query strings
// GET /search?q=express&page=2&limit=10
app.get('/search', (req, res) => {
const { q = '', page = '1', limit = '10' } = req.query;
res.json({ q, page: Number(page), limit: Number(limit) });
});
req.query values are always strings (or arrays for repeated keys). Cast them explicitly.
Router for modular routes
Separate route files keep the codebase organized:
// routes/users.js
import { Router } from 'express';
const router = Router();
router.get('/', listUsers);
router.post('/', createUser);
router.get('/:id', getUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
export default router;
// app.js
import usersRouter from './routes/users.js';
app.use('/api/users', usersRouter);
// Now: GET /api/users, POST /api/users, GET /api/users/:id ...
Router instances can have their own middleware that only applies to that subtree.
Request object
| Property / Method | Description |
|---|---|
req.params |
URL route parameters (strings) |
req.query |
Parsed query string (strings/arrays) |
req.body |
Parsed request body (requires middleware) |
req.headers |
Request headers (lowercase keys) |
req.get('Header') |
Get a specific header |
req.method |
HTTP method ('GET', 'POST', …) |
req.path |
URL path without query string |
req.originalUrl |
Full URL including query string |
req.ip |
Client IP (trust proxy must be set for proxies) |
req.protocol |
'http' or 'https' |
req.hostname |
Hostname without port |
req.is('json') |
Check Content-Type |
req.cookies |
Cookies (requires cookie-parser) |
Response object
// JSON — most common for APIs
res.json({ id: 1, name: 'Alice' });
// With status
res.status(201).json({ id: 1 });
res.status(400).json({ error: 'Bad request' });
res.status(404).json({ error: 'Not found' });
// Plain text
res.send('Hello');
// HTML
res.send('<h1>Hello</h1>');
// Redirect
res.redirect('/login'); // 302 by default
res.redirect(301, '/new-path'); // permanent
// File download
res.download('/path/to/file.pdf', 'report.pdf');
// Send a file (inline)
res.sendFile(path.resolve('./public/index.html'));
// Set headers
res.set('Cache-Control', 'no-store');
res.set({ 'X-Request-Id': id, 'X-Version': '1.0' });
// Set cookie
res.cookie('sessionId', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
});
// End response with no body
res.sendStatus(204); // 204 No Content
Middleware
Middleware functions run before route handlers. They receive (req, res, next) and call next() to continue, or next(err) to trigger error handling.
Custom middleware
// Logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next(); // must call next() or the request hangs
});
// Auth guard for a specific route prefix
app.use('/api/admin', (req, res, next) => {
const token = req.get('Authorization')?.replace('Bearer ', '');
if (!token || !verifyToken(token)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
// Attach user to request
app.use(async (req, res, next) => {
try {
const token = req.get('Authorization')?.split(' ')[1];
if (token) req.user = await getUserFromToken(token);
next();
} catch (err) {
next(err);
}
});
Middleware order matters
app.use(express.json()); // 1. parse body first
app.use(requestLogger); // 2. log
app.use('/api', authMiddleware); // 3. protect routes
app.use('/api', apiRouter); // 4. route handlers
app.use(notFoundHandler); // 5. 404 fallback
app.use(errorHandler); // 6. error handler (last)
Popular third-party middleware
npm install cors helmet morgan cookie-parser express-rate-limit
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import cookieParser from 'cookie-parser';
import rateLimit from 'express-rate-limit';
app.use(helmet()); // security headers
app.use(cors({ origin: 'https://myapp.com' }));
app.use(morgan('combined')); // request logging
app.use(cookieParser()); // parse cookies
app.use(rateLimit({ windowMs: 60_000, max: 100 })); // rate limit
Error handling
Async wrapper pattern
Without a wrapper, thrown errors in async route handlers crash the process unhandled:
// Utility — wrap async handlers so errors reach Express error middleware
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
}));
Alternatively, install express-async-errors and it patches Express automatically.
Error handling middleware
Error middleware has four parameters — Express identifies it by the err argument:
// Must be defined AFTER all routes
app.use((err, req, res, next) => {
const status = err.status ?? err.statusCode ?? 500;
const message = err.message ?? 'Internal Server Error';
// Don't leak stack traces to clients
if (status === 500) console.error(err);
res.status(status).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
});
Custom HTTP errors
class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
// In a route handler
throw new HttpError(422, 'Email already in use');
REST API patterns
A complete CRUD API for a resource:
// routes/posts.js
import { Router } from 'express';
import { asyncHandler } from '../lib/asyncHandler.js';
import * as postsService from '../services/posts.js';
const router = Router();
// GET /api/posts?page=1&limit=20
router.get('/', asyncHandler(async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const posts = await postsService.list({ page: +page, limit: +limit });
res.json(posts);
}));
// GET /api/posts/:id
router.get('/:id', asyncHandler(async (req, res) => {
const post = await postsService.findById(req.params.id);
if (!post) throw new HttpError(404, 'Post not found');
res.json(post);
}));
// POST /api/posts
router.post('/', asyncHandler(async (req, res) => {
const post = await postsService.create(req.body);
res.status(201).json(post);
}));
// PATCH /api/posts/:id
router.patch('/:id', asyncHandler(async (req, res) => {
const post = await postsService.update(req.params.id, req.body);
if (!post) throw new HttpError(404, 'Post not found');
res.json(post);
}));
// DELETE /api/posts/:id
router.delete('/:id', asyncHandler(async (req, res) => {
await postsService.remove(req.params.id);
res.sendStatus(204);
}));
export default router;
Security essentials
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
// Security headers (CSP, HSTS, X-Frame-Options, etc.)
app.use(helmet());
// Disable fingerprinting
app.disable('x-powered-by'); // helmet does this too
// Trust proxy (needed for req.ip behind Nginx/load balancer)
app.set('trust proxy', 1);
// Global rate limit
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
}));
// Strict CORS
app.use(cors({
origin: ['https://myapp.com', 'https://www.myapp.com'],
credentials: true,
}));
// Limit body size (default is 100kb — increase only if needed)
app.use(express.json({ limit: '10kb' }));
Never interpolate req.body or req.params directly into SQL or shell commands. Use parameterized queries and validated input.
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
Missing next() in middleware |
Request hangs forever | Always call next() or send a response |
| Error middleware with 3 params | Express treats it as normal middleware | Always use (err, req, res, next) — four params |
async route without try/catch |
Uncaught rejection crashes process | Use asyncHandler wrapper or express-async-errors |
res.json() after res.json() |
"Cannot set headers after sent" error | Return after sending: return res.json(...) |
| No body parser | req.body is always undefined |
Add app.use(express.json()) before routes |
| Route order wrong | Specific route shadowed by wildcard | Define specific routes before app.all('*', ...) |
6 FAQ
Does Express work with TypeScript?
Yes. Install @types/express and use Request, Response, NextFunction types. For typed req.body, use generics: Request<Params, ResBody, ReqBody, Query>.
What's the difference between app.use and app.get?app.use matches any HTTP method and is prefix-based (/api matches /api/users). app.get only matches GET and requires an exact path match (or route pattern).
How do I serve a React/Next.js SPA with Express?
Serve the build folder with express.static, then add a fallback route that sends index.html for all unmatched paths so client-side routing works.
How do I handle file uploads?
Use multer middleware. It parses multipart/form-data, saves files to disk or memory, and makes them available on req.file or req.files.
How do I add HTTPS to Express?
In production, terminate TLS at the reverse proxy (Nginx, Caddy, AWS ALB). For local development, use https.createServer({ key, cert }, app) with a self-signed cert.
Is Express 5 different from Express 4?
Express 5 (stable as of 2024) adds native async/await error propagation — thrown errors in async handlers automatically reach error middleware without a wrapper. The routing API is mostly compatible.