Introduction & Architecture

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Express 5.3 (npm express 5.3.0, published 2026-10-09; the Express 4 line is at 4.22.3) on Node.js 24 or 26 LTS. Express 5 requires Node.js 18 or later, and Express 4 runs on very old runtimes (the support page lists 0.10 as its floor). The facts were checked on 2026-10-29. The page explains what Express is made of and where it fits, before the rest of the section builds the Bookshelf — a small books/authors/reviews service used as the running scenario, the same domain as in the Node.js, NestJS, FastAPI and Next.js sections.

What Express is

Express describes itself as a fast, unopinionated, minimalist web framework for Node.js. In practice it is a thin layer over the node:http module with three jobs:

  • Routing — choose a handler from the HTTP method and the URL path, with named parameters and wildcards.

  • Middleware — run a chain of functions of (req, res, next) that read and change the request and response, in the order they were registered.

  • Helpers — extend the Node.js request and response objects with conveniences such as req.query, req.params, res.json(), res.redirect(), res.sendFile() and content negotiation.

Everything else — body parsing, sessions, authentication, validation, templating, databases — is an add-on. That is what "unopinionated" means: Express imposes no project layout, no ORM, no template engine and no dependency-injection container. A team assembles the stack, and the cost of that freedom is that conventions have to be agreed on and written down (see Project Structure & Configuration).

An Express application is a function. express() returns a function with the signature (req, res), which is exactly what http.createServer() expects as a request listener. That single fact explains most of the architecture: the app can be handed to http.createServer(), to https.createServer(), to a test client such as supertest, or to a serverless adapter, and it can itself be mounted inside another app.

History

When What happened

2010

TJ Holowaychuk creates Express on top of Connect, a middleware framework, taking inspiration from the Sinatra framework for Ruby. It quickly becomes the default way to write a web server in Node.js.

2012

Express 3 adds many built-in middleware, all bundled with Connect.

April 2014

Express 4.0 removes the dependency on Connect and strips almost all bundled middleware into separate packages. The router becomes its own concern, and the "bring your own middleware" model of today is established. Express 4 stays the dominant line for a decade.

2014 to 2016

Maintenance passes from its creator to StrongLoop, then IBM, and the project joins the Node.js Foundation.

2019

The Node.js Foundation and the JS Foundation merge into the OpenJS Foundation, which hosts Express today.

2024-09-10

Express 5.0.0 is published after a very long beta. It is published under the next dist-tag at first.

2025-03-31

Express 5.1.0 becomes the first 5.x release on the latest dist-tag, which makes npm install express install Express 5.

2025-12-01

Express 5.2.0 and 5.2.1 are published, together with Express 4.22 updates.

2026-10-09

Express 5.3.0 is published. Its History.md top entry lists fixes for security issues in the qs query-string dependency, a proxy-addr update and req.fresh support for the QUERY method.

The dates above come from the npm registry and the GitHub releases page. The project is governed by a Technical Committee, with a security triage team, a documented threat model and a security policy; day-to-day work is done by project captains who own individual repositories. At the time of writing the latest-4 dist-tag still points at 4.22.3, and no Express 6 has been announced, so this section does not document one. See Versions, Support & LTS for the release and support policy.

The module family

Express itself is small. Its package.json lists dozens of dependencies, but the ones that define its behaviour belong to a handful of GitHub organizations:

  • expressjs — the framework and the middleware modules the project maintains (express, body-parser, cors, multer, morgan, compression, cookie-parser, cookie-session, serve-index, and others).

  • pillarjs — the low-level building blocks shared with other frameworks (router, path-to-regexp, send, finalhandler, parseurl, cookies, csrf, resolve-path).

  • jshttp — small HTTP utility libraries (http-errors, content-type, mime-types, accepts, negotiator, proxy-addr, type-is, statuses, etag, fresh, range-parser, on-finished).

The modules that matter most for an application, with the versions Express 5.3.0 depends on:

Module Version range Role in Express

router

^2.2.0

Holds the layers, matches the path and method, runs handlers, and handles next(), next('route') and errors. It provides the Router() objects and the application’s own routing.

path-to-regexp

8.x (through router)

Compiles path strings such as /books/:id or /files/{*path} into matchers. Express 5 uses version 8, whose syntax is stricter than the one in Express 4.

body-parser

^2.3.0

Implements express.json(), express.urlencoded(), express.raw() and express.text().

serve-static

^2.2.0

Implements express.static(); it delegates file streaming to send.

send

^1.1.0

Streams a file with ETag, Last-Modified, range requests and conditional GET. res.sendFile() and res.download() rely on it.

finalhandler

^2.1.0

The last-resort handler. It answers 404 when nothing handled the request and renders the default error response when a handler passed an error to next().

proxy-addr

^2.0.7 or later

Resolves the client address behind proxies according to the trust proxy setting, which drives req.ip, req.ips, req.protocol and req.hostname.

qs

^6.16.0

Parses nested query strings when the query parser setting is extended.

cookie

^0.7.1

Parses and serialises cookie headers; used by res.cookie() and req.cookies middleware.

Because these are ordinary npm packages, any of them can be used on its own, for instance finalhandler and router inside a plain node:http server, or path-to-regexp in a build tool.

Utility modules

The official Utility modules page lists the pillarjs packages that Express and many other projects reuse:

Package Purpose

cookies

Gets and sets HTTP cookies, with optional signing through Keygrip to prevent tampering. It works with plain node:http or as middleware.

csrf

Creates and verifies CSRF tokens, for building custom CSRF middleware.

finalhandler

The final step that responds when no other handler has.

parseurl

Parses a request URL, caching the result so repeated calls are cheap.

path-to-regexp

Turns an Express-style path such as /user/:name into a regular expression.

resolve-path

Resolves a relative path against a root with validation, which is the basis of safe static file serving.

router

A simple middleware-style router, usable without Express.

send

Streams files as HTTP responses with range requests, conditional GET and events.

The official middleware list (body-parser, cors, multer, morgan and the rest) is a different set; it is covered in Official Middleware Modules.

The layer stack

The following figure shows where each piece sits. Your code only touches the top layer. Everything beneath it is replaceable or reusable on its own, and Node.js and libuv do the sockets, parsing and event loop.

The Express layer stack from your code through the Express app and router, building-block modules, node:http, and Node.js with libuv

How a request flows through the stack

When a request arrives, node:http parses the request line and headers, creates an IncomingMessage and a ServerResponse, and calls the app function. Express then adds its own helpers to both objects and hands them to the router. The router keeps an ordered list of layers; each layer is either a middleware function (matched by path prefix) or a route (matched by method and path). The router walks that list, calling next() to move on, until a handler sends a response. If the list is exhausted, or an error is passed to next(err), finalhandler produces the response.

sequenceDiagram autonumber participant C as Client participant N as node:http participant A as Express app participant R as Router and layers participant H as Route handler participant F as finalhandler C->>N: HTTP request N->>A: app(req, res) A->>R: handle request loop each matching layer in registration order R->>R: middleware, for example express.json() end alt a route matches R->>H: handler(req, res, next) H-->>C: res.json(...) else no layer sent a response R->>F: done() F-->>C: 404 Not Found else a handler called next(err) R->>F: done(err) F-->>C: 500 or err.status end

The ordering rule is the most important thing to remember: a layer registered after the one that handles the request never runs, and a body parser registered after the route that needs req.body does not help. Error-handling middleware has four parameters (err, req, res, next) and is skipped on the success path; it must be registered after the routes it protects. A default error handler (finalhandler) exists even when the application defines none.

Express 5 changed a few details of this flow compared with Express 4:

  • A handler that returns a rejected promise, or is an async function that throws, has its error forwarded to next() automatically. Express 4 did not, so every async handler needed a try/catch or a wrapper.

  • req.query is computed lazily through a getter; Express 4 ran a query-parsing middleware on every request. The default query parser setting is simple in Express 5 (it was extended in Express 4).

  • Path syntax follows path-to-regexp 8: a bare * is no longer valid and must be a named wildcard such as /{*splat}; optional segments use braces; regular-expression sub-patterns in strings are no longer supported.

  • express.urlencoded() defaults to extended: false, and req.body is undefined when no body parser matched.

The hello-world application below runs the whole flow with the smallest amount of code.

// bookshelf-api/server.js -- run with: node --watch server.js
import express from 'express';

const app = express();

app.use(express.json());

app.get('/api/v1/books/:id', (req, res) => {
  res.json({ id: req.params.id, title: 'Placeholder title' });
});

app.use((req, res) => {
  res.status(404).json({ error: 'Not found' });
});

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});

const server = app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
  console.log('Bookshelf API listening on http://localhost:3000');
});

process.on('SIGTERM', () => server.close());

Express versus raw node:http

Everything Express does can be written against node:http. Seeing the same endpoint both ways shows what the layer buys you.

// raw-http.js -- the same GET /api/v1/books/:id without Express
import http from 'node:http';

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  const match = /^\/api\/v1\/books\/([^/]+)$/.exec(url.pathname);

  if (req.method === 'GET' && match) {
    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({ id: match[1], title: 'Placeholder title' }));
    return;
  }

  res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
  res.end(JSON.stringify({ error: 'Not found' }));
});

server.listen(3000);

Source: Node.js: HTTP

Raw node:http leaves URL parsing, route matching, header and status handling, JSON serialisation, body reading, content negotiation, cookies, static files, ETags, range requests, error handling and proxy awareness to the application. Express supplies each of them as a tested module with documented options, and keeps the underlying objects available: req and res are still the Node.js IncomingMessage and ServerResponse, so streams, req.on('data') and res.write() work as usual. The costs are a small per-request overhead from layer traversal, a fixed handler signature, and the dependency tree.

Concern Raw node:http Express 5

Route matching

Hand-written comparisons or regular expressions

app.get('/books/:id', ...), routers and path parameters

JSON body

Collect chunks, concatenate, JSON.parse, handle errors and size limits

express.json({ limit })

JSON response

writeHead plus JSON.stringify plus end

res.json(data)

Static files

fs.createReadStream plus headers, ETags, ranges

express.static() and res.sendFile()

Errors

Manual, per request

Error-handling middleware and finalhandler

Cross-cutting behaviour

Not provided

Middleware chains with app.use()

Express versus Koa and Fastify

Express is not the only way to build a Node.js web service; its two best-known alternatives make different trade-offs.

Aspect Express 5 Koa Fastify

Style

(req, res, next) middleware chain

async (ctx, next) middleware that "unwinds" with await next()

Plugin and hook architecture with encapsulated contexts

Built in

Router, static files, body parsing, view rendering, response helpers

Almost nothing; a router, body parser and static serving are separate packages

Router, JSON schema validation and serialisation, logger (Pino), hooks

Async errors

Forwarded automatically since Express 5

Native, through try/catch around await next()

Native, through async hooks and handlers

Request and response

Extended Node.js req and res

A ctx object wrapping them

request and reply wrappers

Performance focus

Compatibility and a very large ecosystem

Small core

Throughput and low overhead; schema-based serialisation

Ecosystem

By far the largest set of middleware

Smaller, with Koa-style middleware

Large and growing plugin catalogue

The same endpoint in the other two frameworks, for comparison only:

// koa-example.js
import Koa from 'koa';
import Router from '@koa/router';

const app = new Koa();
const router = new Router();

router.get('/api/v1/books/:id', (ctx) => {
  ctx.body = { id: ctx.params.id, title: 'Placeholder title' };
});

app.use(router.routes());
app.listen(3000);
// fastify-example.js
import Fastify from 'fastify';

const app = Fastify({ logger: true });

app.get('/api/v1/books/:id', async (request) => {
  return { id: request.params.id, title: 'Placeholder title' };
});

await app.listen({ port: 3000 });

Choose Express when the team already knows it, when a particular middleware or integration exists only for Express, or when simplicity and a huge body of examples matter more than raw throughput. Choose Fastify when schema validation and performance are priorities, and Koa when a minimal async-first core appeals. For a longer comparison with Fastify see Web Frameworks: Express and Fastify, and for frameworks that are built on top of Express (NestJS, LoopBack, KeystoneJS) see Frameworks Built on Express & Alternatives.

Express in the MEAN, MEVN and MERN stacks

Full-stack JavaScript stacks are named after their parts. All of them share the same back end: *M*ongoDB for storage, *E*xpress as the HTTP layer and *N*ode.js as the runtime. They differ only in the front-end framework.

Stack Front end Role of Express

MEAN

Angular (originally AngularJS)

Serves a JSON API and, in production, the compiled Angular build as static files.

MEVN

Vue.js

Serves a JSON API to a Vue single-page application, usually with CORS during development and a history fallback in production.

MERN

React

Serves a JSON API, optionally renders React on the server, and can mount GraphQL or WebSocket endpoints on the same HTTP server.

In every variant, Express is the seam between the browser and the database: it validates input, authenticates users, talks to MongoDB (usually through Mongoose) and returns JSON. The pattern has stayed the same since the early 2010s, but the details have not: the front-end frameworks have moved on, the build tools changed from Gulp and Grunt to Vite, and MongoDB is now typically hosted on MongoDB Atlas. This section keeps the back end half of those stacks — see MongoDB & Mongoose, SPAs & Server-Side Rendering and CORS — and Express is just as useful with PostgreSQL, SQLite or any other store.

Glossary

The official glossary defines a short vocabulary that the rest of the section uses:

Term Meaning

application (app)

A program that uses the Express API on Node.js; also the object returned by express().

Express

The preferred spelling of the framework’s name (the glossary accepts "Express.js" too).

libuv

The multi-platform library for asynchronous I/O that Node.js is built on.

middleware

A function the routing layer calls before the final request handler, sitting between the raw request and the route.

Node.js

The JavaScript runtime with non-blocking I/O and a single-threaded event loop that Express runs on.

request

The HTTP message a client sends to the server.

response

The HTTP message the server returns, carrying a status and possibly content.

route

The part of a URL that identifies a resource, for example the path of a book.

router

An object that groups routes and middleware; documented in the API reference as Router.

API

Application programming interface.

Where the books are outdated

Four published books on Node.js full-stack development were consulted for the concepts on this page (their publisher pages are listed under References). They all describe the Express 4 era, and some statements no longer hold.

What the books present Today

Express 4.0 to 4.18 as the current version; none can show Express 5

Express 5.3 is the default on npm; Express 4 is on the latest-4 tag and still receives fixes.

npm install -g express to get a command-line tool

The CLI is the separate express-generator package, run with npx, and it still generates an Express 4, Jade-era application.

app.use(bodyParser()) or a separately installed body-parser for every app

express.json() and express.urlencoded() are built in; the combined bodyParser() helper no longer exists.

Handlers that rely on res.send(status, body)

Removed in Express 5; use res.status(code).send(body).

Old-style stacks (AngularJS, Bower, Gulp, Grunt, Yeoman) as part of MEAN or MERN

These are end of life, archived or deprecated; current stacks use Vite and maintained front-end frameworks.

var, CommonJS and callbacks everywhere

Express 5 works with ES modules and async/await; examples in this section use ESM and node: imports.

The detailed corrections for each topic are on the relevant pages, and Migrating & What Changed collects them in one place.

What changed recently

  • Express 5.0 (2024-09-10) — native promise handling in handlers, path-to-regexp 8 route syntax, lazily computed req.query, removal of long-deprecated APIs, and Node.js 18 as the minimum.

  • Express 5.1 (2025-03-31) — the first 5.x on the latest tag, so new installations get Express 5 by default.

  • Express 5.2 (2025-12-01) and 5.3 (2026-10-09) — security-driven dependency updates (qs, proxy-addr) and smaller API additions such as QUERY method support in req.fresh.

  • Official website (2026) — rebuilt with new URLs ending in /; unversioned documentation URLs show 5.x, and Express 4 and 3 documentation live under /en/4x/ and /en/3x/.

  • Deprecated middleware (2025) — csurf, connect-multiparty and path-match were deprecated; see Security Best Practices for maintained alternatives.

Common pitfalls

  • Registering a body parser after the route that reads req.body, which leaves req.body as undefined.

  • Forgetting return after sending a response in a callback, which sends a second response and fails with ERR_HTTP_HEADERS_SENT.

  • Assuming Express 4 path syntax such as /* still works; in Express 5 it throws at startup.

  • Calling listen() inside the module that tests import, which opens a port during tests; keep app and server in separate files.

  • Treating Express as a framework with batteries: input validation, security headers, rate limiting and CSRF protection all have to be added deliberately.