Toolmingo
Guides8 min read

Express.js Cheat Sheet: Routing, Middleware, and REST APIs

A complete Express.js cheat sheet — routing, middleware, request/response, error handling, REST API patterns, and security tips. Copy-ready examples for Node.js web development.

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=fooreq.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.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools