express review
Express 5.2.1 is a Node web framework whose main abstraction is an ordered list of middleware and route handlers. It supplies routing, body parsers, response helpers, static file delivery, and hooks for template engines, while leaving databases, validation, authentication, and project layout to other modules. Express 5 sends rejected promises from async handlers to error middleware and requires newer path-to-regexp syntax for wildcards and optional segments. Our package check found CommonJS code that worked through require() and ESM import, but no bundled TypeScript declarations.
Express 5.2.1 installed in 1.3 seconds but left 65 packages and no bundled types in our sandbox, so its real advantage is middleware compatibility rather than a tiny dependency footprint. Pick it for conventional Node services; prefer Fastify or Hono when schemas or multi-runtime deployment define the project.
We installed it
| Install | ✓ · 1.3s | 65 packages on disk · 5 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does express install cleanly?
Yes. In a fresh container with an empty cache, npm install express finished in 1 seconds, leaving 65 packages and 5 MB on disk. npm audit reported no known vulnerabilities.
Can express run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does express work with both ESM and CommonJS?
Yes. Both import 'express' and require('express') worked in Node 22 in our run. The package is published as CommonJS.
Does express include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
express or fastify: which should you use?
fastify: Use it for schema-led JSON APIs with integrated validation and serialization. Express 5.2.1 installed in 1.3 seconds but left 65 packages and no bundled types in our sandbox, so its real advantage is middleware compatibility rather than a tiny dependency footprint.
When should you not use express?
Schemas should control validation and serialization at every route; Fastify builds those operations into its route model
Discussed on
- hnExpressJS 5.044 points
- hnExpressJS 5.0 Released21 points
- hnExpress.js Releases Version 55 points
- hnExpressJS 5.0 Released4 points
- hnIssue for Express 5 re-opened by maintainer4 points
Use it if
- An existing service already depends on Express routers or middleware such as Passport, multer, or express-session
- A small Node API benefits from explicit middleware order and very few framework conventions
- The team values an HTTP API that many Node developers already recognize
- An Express 4 application needs a staged move to promise-aware Express 5 handlers
- Schemas should control validation and serialization at every route; Fastify builds those operations into its route model
- The deployment target includes Workers, Deno, or several non-Node runtimes; Express requires Node 18 or newer
- Built-in dependency injection, logging, configuration, or database integration is expected; Express does not choose them
- TypeScript types must arrive in the runtime package; our install found none, so typed projects normally add @types/express
- Client-side bundling is required; esbuild could not produce a browser bundle because Express depends on Node server facilities
Setup reality
We installed Express 5.2.1 in a fresh Node 22 sandbox in 1.3 seconds. It left 65 packages and 5 MB on disk, and npm audit found 0 known vulnerabilities. Express declares 28 direct dependencies, 0 peer dependencies, and Node 18 or newer. Its own package is 104 KB unpacked under the MIT license. The CommonJS package has no exports map, though require() and ESM import both worked. We found no bundled TypeScript types.
Our esbuild browser attempt failed because the dependency graph uses Node server modules. Express belongs on the server. Middleware order is executable configuration: put express.json() before code that reads req.body, authentication before protected routers, the 404 handler after known routes, and four-argument error middleware last. A handler that neither sends a response nor calls next() leaves its request open. Parsing JSON also does not validate the resulting object.
Express 5 forwards a rejected route-handler promise to error middleware. Detached promises and work started after a response are outside that path. Preserve the err, req, res, next signature so Express recognizes the error handler, and delegate to the default handler when headers have already been sent. Route matching changed as well: wildcards need names, and braces replace several older optional patterns. Run route tests against the version 5 migration guide.
No credentials or framework config file are required, but production decisions remain. Set trust proxy only for the actual proxy chain before using req.ip or secure cookies. Choose body limits, CORS rules, request timeouts, rate controls, and validation deliberately. Save the http.Server returned by listen(), stop accepting traffic on SIGTERM, and close database or queue connections after in-flight work receives a bounded shutdown window.
Patterns
Start a server and keep its handle start-server
import express from 'express';
const app = express();
app.get('/', (req, res) => res.send('Hello'));
const server = app.listen(3000);The returned http.Server is needed for a bounded shutdown; app.listen by itself does not close database pools.
Parse a limited JSON body parse-json
app.use(express.json({ limit: '1mb' }));
app.post('/items', (req, res) => {
res.status(201).json({ name: req.body.name });
});Register the parser before the route, then validate req.body because successful parsing proves only that the input was JSON.
Mount feature routes under one prefix mount-router
const router = express.Router();
router.get('/:id', (req, res) => res.json({ id: req.params.id }));
app.use('/users', router);The router receives middleware that was registered before its mount point; later app middleware does not run first.
Continue an ordered middleware chain write-middleware
function requestId(req, res, next) {
req.requestId = crypto.randomUUID();
next();
}
app.use(requestId);Call next once or finish the response. Doing neither leaves the connection pending.
Let Express 5 catch a rejected handler handle-async-error
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
res.json(user);
});Express 5 forwards a rejected handler promise; detached background promises still need their own failure handling.
Install final error middleware handle-errors
app.use((err, req, res, next) => {
if (res.headersSent) return next(err);
res.status(err.status || 500).json({ error: 'request failed' });
});All 4 parameters are required for Express to classify this function as error middleware.
Answer unmatched routes after routers return-404
app.use((req, res) => {
res.status(404).json({ error: 'not found' });
});Register this after every intended route and before the final error handler.
Stop accepting requests on SIGTERM shutdown-server
process.on('SIGTERM', () => {
server.close(async () => {
await db.close();
process.exit(0);
});
});Add an application-specific deadline because server.close waits for existing connections and can otherwise delay termination.
Alternatives
More web backend guides
urllib3 · requests · ws · anyio · undici · httpx · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

