cheat sheet
express
Package-level reference for Express on npm — install, middleware model, Node support, 4-to-5 migration, and production deployment.
express
What it is
express is the original minimalist Node.js web framework — a thin wrapper over Node's http module that adds routing, a middleware pipeline, and a request/response API with view-engine hooks. It is intentionally unopinionated: validation, ORMs, logging, and templating are all add-ons rather than core concerns.
Reach for express when you need the largest middleware ecosystem in JavaScript, when a colleague will read the code without learning a new framework, or when you're maintaining one of the millions of legacy Express apps already in production. Choose fastify or hono for new high-throughput work; choose nestjs for opinionated DI-heavy services.
Install
Express is a library — it lives in your application's dependencies and runs as a long-lived Node process.
npm install express
Output: added express to dependencies
pnpm add express
Output: added 1 package, linked from store
yarn add express
Output: added express
bun add express
Output: installed express
For TypeScript projects, install the type package as a dev dependency — Express does not ship types itself.
npm install --save-dev @types/express
Output: added @types/express to devDependencies
Versioning & Node support
The current line is express@5.x (released 2024 after years of preview); express@4.x remains the default of most existing tutorials.
express@5— Node 18+. Async middleware errors propagate to the error handler withoutnext(err). Promise rejections in handlers are caught automatically. Several deprecated APIs (res.sendfile,req.param,app.del) were removed.express@4— Node 6+. The dominant version on npm by download count. Still maintained for security patches.express@3— historical. Do not start new work here.
Express follows semver loosely — many minor releases include type-definition or middleware-default tweaks. Pin minor in production ("express": "5.x") and run npm outdated quarterly.
Package metadata
- Maintainer: OpenJS Foundation / Express TC (community-led since 2014)
- Project home: github.com/expressjs/express
- Docs: expressjs.com
- npm: npmjs.com/package/express
- License: MIT
- First released: 2010
- Downloads: ~30 million+ weekly — still the most-downloaded Node web framework by an order of magnitude.
Peer dependencies & extras
Express has zero peer dependencies. Middleware is published as separate npm packages.
Commonly-paired middleware:
cors— CORS handling for cross-origin requestshelmet— secure HTTP headersmorgan— request loggingcompression— gzip / brotli compressioncookie-parser— cookie parsingexpress-session— server-side sessionsexpress-rate-limit— rate limitingmulter— multipart/form-data uploadsexpress-validator— schema validationpassport— authentication strategies
Express 4.16+ bundles body-parser internally — use express.json() and express.urlencoded() instead of the separate package. See the dedicated body-parser article for the standalone use case.
Alternatives
| Framework | Trade-off |
|---|---|
fastify | Schema-driven validation, 2-3× faster, first-class types. Smaller ecosystem. |
hono | Edge-first; runs on Workers / Deno / Bun. Web-standard Request/Response. |
koa | Express creators' rewrite. Async-native, smaller core, weaker ecosystem. |
nestjs | Class + decorator + DI architecture. Heavier, opinionated, TypeScript-first. |
h3 | Powers Nuxt / Nitro. Edge-friendly, composable utilities. |
restify | Older REST-focused framework. Largely superseded. |
Real-world recipes
Minimal HTTP app
The smallest useful Express app — bind, serve, listen. Demonstrates the core app.get / res.send pattern.
import express from "express";
const app = express();
app.get("/", (req, res) => res.send("ok"));
app.listen(3000, () => console.log("http://localhost:3000"));
Output: curl localhost:3000 returns ok; server logs the listen URL.
Route + middleware chain
Middleware is just a function with (req, res, next). Mount with app.use (global) or pass as additional args to a route (per-route). Order matters — middleware runs in registration order until one ends the response.
import express from "express";
const app = express();
const requireAuth = (req, res, next) => {
if (req.headers.authorization !== "Bearer secret") {
return res.status(401).json({ error: "unauthorized" });
}
next();
};
app.get("/public", (req, res) => res.json({ ok: true }));
app.get("/private", requireAuth, (req, res) => res.json({ user: "alice" }));
app.listen(3000);
Output: /public returns 200 unconditionally; /private returns 401 without the bearer header, 200 with it.
JSON body + validation
express.json() parses application/json bodies in-place. Pair with express-validator (or zod) for schema validation. A request without a matching schema should fail before touching the handler.
import express from "express";
import { body, validationResult } from "express-validator";
const app = express();
app.use(express.json({ limit: "100kb" }));
app.post(
"/users",
body("email").isEmail(),
body("age").optional().isInt({ min: 0 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
res.status(201).json({ id: crypto.randomUUID(), email: req.body.email });
}
);
app.listen(3000);
Output: POST /users with {"email":"alice@example.com"} returns 201; missing email returns 400 with structured errors.
Centralised error handler
Express recognises error-handling middleware by arity — four parameters. Place it last; uncaught throws (in Express 5) or next(err) calls (in Express 4) land here.
import express from "express";
const app = express();
app.get("/boom", () => {
throw new Error("kaboom");
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: "internal_error", message: err.message });
});
app.listen(3000);
Output: /boom returns 500 with {"error":"internal_error",...}; stack trace logged server-side.
Static files
express.static serves files from a directory with sensible defaults — ETag, last-modified, max-age. For production, prefer a CDN or reverse proxy, but express.static is fine for small apps.
import express from "express";
import path from "node:path";
const app = express();
app.use("/assets", express.static(path.join(process.cwd(), "public"), {
maxAge: "1d",
immutable: false,
}));
app.listen(3000);
Output: GET /assets/logo.svg returns the file with Cache-Control: max-age=86400.
Production deployment
Express is a long-lived Node process. Treat it like any production service — process supervision, clustering, health checks, log shipping.
Process supervisor: PM2
npm install -g pm2
pm2 start dist/server.js -i max --name api
pm2 startup
pm2 save
Output: runs N workers (one per CPU core), auto-restarts on crash, persists across reboots.
-i max enables Node cluster mode — multiple workers share the listening socket via the OS load balancer. For CPU-bound work this scales linearly with cores.
Behind a reverse proxy
Run Express behind Nginx / Caddy / a cloud load balancer for TLS, HTTP/2, and static-asset caching. Enable trust proxy so Express honours X-Forwarded-* headers.
app.set("trust proxy", 1); // trust first proxy hop
Output: req.ip resolves to the real client IP rather than the proxy's loopback.
Docker
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
Bind to 0.0.0.0 inside containers — app.listen(3000, "0.0.0.0", ...) — the default 127.0.0.1 is unreachable from outside the container.
Health checks
app.get("/healthz", (req, res) => res.json({ ok: true }));
Output: load balancers can poll /healthz without flooding application logs.
Gate noisy access logs at the health-check path via morgan's skip option.
Performance tuning
Express is slower than Fastify or Hono by design — the middleware model has per-call overhead and there's no schema-driven serialization. Most apps are network-bound and never notice.
- Set
NODE_ENV=production. Express disables view caching and verbose error pages otherwise. ~2× throughput improvement on hot paths. - Use
compression()selectively. gzip costs CPU; only enable for large responses. Static assets compressed at the proxy beat in-process compression. - Reuse JSON serialization. Avoid building large response objects per request. Cache where the data permits.
- Cluster across cores. Single-threaded Node leaves CPU on the table. PM2 or
node:clusteris mandatory above a few hundred req/s. - Avoid synchronous middleware. Anything calling
fs.readFileSyncorbcrypt.hashSyncblocks the event loop. Use the async siblings. - Profile with
clinic.js.npx clinic doctor -- node server.jsproduces a diagnostic of event-loop lag, GC, and CPU hot spots.
Version migration guide
The big migration is express@4 → express@5. Most of the breaking changes are pruning, not rewrites.
4 → 5 highlights:
- Promise rejections propagate to the error handler automatically. Drop the
next(err)boilerplate inasynchandlers. - Removed APIs:
res.sendfile(useres.sendFile),req.param(usereq.params/req.query),app.del(useapp.delete),res.json(status, body)(useres.status(s).json(b)). path-to-regexpupgraded. Some route patterns may match differently —/:foo*no longer matches greedily; convert to/{:foo}+or split routes.- Body parsers stricter.
express.json()rejects on syntax errors by default rather than treating them as empty bodies. req.bodyundefined without parser middleware. Removed implicit fallback to{}.router.useno longer accepts trailing slashes in mount paths. Normalize the mount path.
Migration sequence:
npm install express@5
npx eslint --no-eslintrc --rule '{"deprecation/deprecation":"error"}' src/
npm test
Output: ESLint flags removed APIs; fix until clean; test suite catches behavioural drift.
For most apps the migration is mechanical — a few hours of grep and test.
Security considerations
- Always
helmet(). Sets sane defaults for CSP, HSTS, X-Content-Type-Options, X-Frame-Options. One line:app.use(helmet()). - Body-parser limits. Default JSON body limit is 100 KB; raise only when you must, and validate uploads pass through. Unbounded sizes invite memory-exhaustion DoS.
- Cookie security.
httpOnly: trueandsecure: trueon every session cookie.sameSite: "lax"minimum. trust proxycarefully.truetrusts every hop, which spoofs IPs if you're not behind a single trusted proxy. Set the exact hop count (1,2) instead.- Don't leak stack traces. Default error handler emits the stack in development; ensure production returns a sanitized envelope.
- Rate-limit
/login,/signup,/reset-password.express-rate-limitwith stricter window/limit on auth routes blocks credential stuffing. req.queryparsing. Express 5 parses?a[b]=cas a nested object — easy to over-trust. Cast to strings before passing to ORMs.- CSRF tokens. Cookie-based session apps still need CSRF protection. Use
csurfor move to short-lived JWT inAuthorization. npm auditweekly. Express's transitive deps (path-to-regexp, qs) have had CVEs; audit and patch promptly.
Testing & CI integration
Express's testing story is dominated by supertest — wraps the app, makes assertions on responses without binding a port.
import { test, expect } from "vitest";
import request from "supertest";
import express from "express";
const app = express();
app.get("/", (req, res) => res.json({ ok: true }));
test("GET / returns ok", async () => {
const res = await request(app).get("/");
expect(res.status).toBe(200);
expect(res.body).toEqual({ ok: true });
});
Output: 1 passed. Tests run in microseconds; no real TCP socket; concurrent tests don't conflict on ports.
Pair with vitest or jest. CI workflow:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npm test
Ecosystem integrations
| Tool | Role |
|---|---|
passport | Pluggable auth strategies — local, OAuth, SAML, OpenID. |
socket.io | WebSockets layered over an Express HTTP server. |
next.js | Custom server mode can mount Next inside Express, though deprecated in favour of standalone. |
swagger-ui-express | Serve Swagger UI for OpenAPI documents. |
connect-redis | Redis-backed session store for express-session. |
multer | Multipart upload handler. |
graphql-http / apollo-server-express | GraphQL servers on Express. |
express-openapi-validator | Validate requests against an OpenAPI spec. |
Troubleshooting common errors
TypeError: app.use() requires a middleware function — typo in import, or module.exports returns a non-function. Most often: app.use(myMiddleware) where myMiddleware resolved to undefined.
Cannot set headers after they are sent — calling res.send / res.json twice. Most often: forgetting return in an early-exit branch.
PayloadTooLargeError — body exceeded express.json({ limit }). Raise the limit if legitimate, or reject upstream.
Error: Failed to lookup view — using res.render without configuring a view engine. Either set app.set("view engine", "ejs") (or your engine of choice) or switch to res.json.
req.body is undefined — express.json() not mounted before the route. Add app.use(express.json()) near the top.
EADDRINUSE — port already taken. lsof -i :3000 to find and kill, or pass PORT=3001 via env.
MaxListenersExceededWarning — too many event listeners on a single emitter. Often process.on("uncaughtException", ...) added in a request handler. Move to top-level.
404 on a route that should exist — middleware before it didn't call next(). Trace through app.use registrations.
When NOT to use this
- Edge runtimes. Express expects Node's
httpmodule. For Cloudflare Workers / Vercel Edge / Deno Deploy usehonoorh3— they share Express's middleware idiom over WebRequest/Response. - Bun-only deployments. Bun has
Bun.serve, which beats Express by 5-10× on identical handlers. Use Bun's native API if you commit to Bun. - High-throughput APIs (>20k req/s per core). Fastify's schema-driven serializer doubles throughput. The migration cost amortizes quickly.
- You want first-class TypeScript. Express types are community-maintained and historically loose. Fastify / Hono / Nest are TS-first.
- You need built-in DI / module decomposition. Express is functional; for class + DI architecture, NestJS (which can run atop Express) gives you the structure for free.
- GraphQL-only backends. Apollo and
mercurius(Fastify) are GraphQL-native. Express is fine but adds nothing.
See also
- JavaScript: Node runtime — process model, async I/O, modules
- Concept: http — request / response semantics
- Concept: api — REST patterns, status codes, JSON envelopes