Skip to content

What Is Express.js? The Minimalist Web Framework for Node.js

Express.js is a minimalist, unopinionated web framework for Node.js built on routing and middleware — plus what Express 5 changed from Express 4.

Tuan Tran Van
10 min read
Contents (9 sections)
  1. What is Express.js?
  2. What does a minimal Express application look like?
  3. Middleware: the core mechanism of Express
  4. Routing and express.Router
  5. Error handling in Express
  6. What changed between Express 4 and Express 5?
  7. Is Express still well maintained?
  8. When should you choose Express — and when not?
  9. References

Express.js is a fast, unopinionated, minimalist web framework for Node.js. It gives you a thin layer of fundamental web features, routing and middleware, without hiding the core capabilities of the Node.js runtime.

Running JavaScript on both the client and the server removes the mental context switch between the two halves of a project. Express gives you structure for server-side logic while keeping the full flexibility of plain JavaScript.

Express.js as a thin, minimalist framework layer sitting on top of the Node.js runtime

What is Express.js?

Express.js is a lightweight routing and middleware framework that runs inside the Node.js runtime. Node.js supplies the cross-platform environment for executing JavaScript on a server, including low-level APIs for the file system and HTTP. Express adds the high-level structure a web application needs on top of it. Node runs on Linux, macOS, Windows, Solaris, FreeBSD, and OpenBSD, so Express is a viable target almost anywhere you can deploy.

The four pillars of Express.js resting on the Node.js runtime: web apps, APIs, performance, and middleware

The framework is defined by its unopinionated philosophy. Opinionated frameworks mandate a directory structure and an architectural pattern. Express mandates neither. You choose your own database layer, your own template engine, and your own middleware, and you carry full responsibility for how the architecture holds together. That is the best thing about Express and the worst thing about it, depending on the week. Without deliberate conventions agreed up front, an Express codebase drifts toward sprawl faster than most.

What you get in the box is narrow. Express maps HTTP verbs (GET, POST, PUT, DELETE) to the URI endpoints you define. It plugs into rendering engines such as Pug or EJS, which generate HTML by injecting data into templates. It manages application configuration: environment settings, port definitions, and route sensitivity. That is close to the whole list.

Express preserves the performance profile of Node.js by staying inside its single-threaded, event-driven execution model. That architecture leans on non-blocking asynchronous APIs, which is what keeps throughput high under production load. The framework's job here is mostly to stay out of the way, and it does.

What does a minimal Express application look like?

A "Hello World" server shows how little of the API you need to start.

javascript
const express = require("express");
const app = express();
const port = 3000;
 
app.get("/", (req, res) => {
  res.send("Hello World!");
});
 
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

The application object

Importing the express module and invoking it creates the app object. Everything else in the application hangs off it: the methods for routing requests, registering middleware, and configuring global settings.

Route definition and callbacks

The app.get() method defines a handler for HTTP GET requests at the site root. The callback takes the standard req (request) and res (response) arguments. Calling res.send() sends the response body and ends the request-response cycle. That is the whole contract.

Starting the server

The app.listen() method binds the application to a port and an optional hostname, then starts listening. Express 5 changed how this callback behaves on failure: where Express 4 threw server errors, Express 5 passes them to your callback as its first argument. Check for that error before logging a success line, or a port collision will look like a clean startup.

Middleware: the core mechanism of Express

Middleware functions are the fundamental building block of Express. They sit in the pipeline between the incoming request and the outgoing response, which is where logging, authentication, and body parsing belong. Learn middleware properly and you have learned most of the framework.

A request travelling through a chain of (req, res, next) middleware functions before reaching the response, with the built-in, third-party, and custom categories

The middleware signature

Middleware follows the (req, res, next) signature. These functions can run code and modify the request and response objects, and each one must either end the request-response cycle or call next() to pass control forward. Do neither and the request simply hangs — the client waits until it times out.

Categories of middleware

  • Built-in: Express ships five middleware functions in core: express.static for static assets, express.json for JSON payloads, express.urlencoded for form data, and express.raw and express.text for Buffer and text payloads.
  • Third-party: packages installed from npm, such as morgan for request logging or cookie-parser for reading cookies.
  • Custom: functions you write for your own domain logic, such as an authorizer.

The stack and execution order

Middleware and routing functions execute in the exact order they are declared. This sequence matters more than it first appears. Register express.json() after your route handlers and req.body is undefined inside them, with nothing in the logs to explain why. Register an error handler above your routes and it will never catch anything they throw.

The next() function

The next() function drives the stack forward, telling Express to move to the next function in the chain. Passing an argument to next() signals an error, unless that argument is the string 'route'. Express then skips every remaining non-error-handling middleware.

Routing and express.Router

Routing is the part of Express you touch every day: which URI, which verb, which handler.

Express mapping HTTP verbs to paths via app.METHOD(), with express.Router splitting feature areas into mountable mini-apps

Basic routing and HTTP verbs

Express maps endpoints with app.METHOD(), covering all the standard HTTP verbs. The app.all() method is a special case that applies logic to every verb at a given path, which is useful for forcing authentication on a protected endpoint.

Path matching and parameters

Express 5 uses path-to-regexp v8 for matching route paths, and its syntax is stricter than Express 4's. Route paths can be literal strings, regular expressions, or carry named parameters: /users/:userId populates req.params.userId. Regex characters are no longer supported inside string paths, so use an array of paths or a genuine regular expression instead.

express.Router for modularity

The express.Router class creates mini-apps: modular, mountable route handlers that keep a growing codebase organized. Split each feature area into its own file, then mount it with app.use(). One common snag is that a nested router does not inherit its parent's path parameters by default. Passing { mergeParams: true } to the Router constructor restores access to them.

Chainable route handlers

The app.route() method defines multiple handlers for a single URI in one place. Chaining this way cuts repetition and removes a common source of typos in path strings.

Error handling in Express

Express provides a dedicated middleware tier for catching both synchronous and asynchronous errors.

An error falling through to a four-argument (err, req, res, next) middleware declared last in the stack, then to Express's default error handler

The four-argument signature

Error-handling middleware is declared with four arguments: (err, req, res, next). Express recognizes that specific signature and invokes these functions only when an error travels down the pipeline. Omit err and Express treats the function as ordinary middleware, silently skipping your error logic.

The default error handler

Express includes a built-in error handler at the end of the stack. In development it returns the full stack trace. With NODE_ENV=production it suppresses the trace, which is the behavior you want, because shipping a stack trace to a client is a real security problem. If an error surfaces after headers have already been sent, call next(err) to delegate to the default handler so it can close the connection cleanly.

Synchronous versus asynchronous catching

Express 5 automatically catches errors from handlers that return a promise, async functions included. A rejected promise is forwarded to your error handler with no try/catch around it. Legacy callback-based APIs such as fs.readFile are the exception. There you still catch the error yourself and pass it to next(err).

Placement

Custom error handlers must be declared last, after every other app.use() and route definition, so they sit downstream of everything that might fail.

What changed between Express 4 and Express 5?

Express 5 modernizes the framework and pays down technical debt rather than adding features, but several breaking changes need real migration work. Most of it is mechanical. A few parts are not.

Express 4 versus Express 5: removed legacy methods placed side by side with their replacements

Infrastructure and automation

Express 5 requires Node.js 18 or higher. Codemods automate most of the mechanical migration, and @expressjs/v5-migration-recipe runs the full recipe across a codebase.

Removed and replaced methods

  • app.del() is gone; use app.delete().
  • The generic req.param() is removed. Read req.params, req.body, or req.query explicitly.
  • res.sendfile() became the camel-cased res.sendFile().
  • Pluralized methods: req.acceptsCharset() is now req.acceptsCharsets(), and the same applies to Encodings and Languages.
  • express.static.mime is no longer exported; use the standalone mime-types package.
  • express.urlencoded now defaults extended to false.

Route matching and path syntax

After the upgrade to path-to-regexp v8, wildcards must be named, so /* becomes /*splat. The optional ? character is replaced by braces, which turns /:file.:ext? into /:file{.:ext}.

MIME type changes

JavaScript files are now served as text/javascript instead of application/javascript, following an update to the underlying mime-db dependency.

Improved promise handling

Express 5 forwards rejected promises to error-handling middleware automatically. If an async handler throws or an awaited promise rejects, the error reaches your handler without a manual try/catch wrapping the asynchronous logic.

Is Express still well maintained?

In 2024 Express was raised to "Impact Project" status under the OpenJS Foundation, recognition of how much of the JavaScript ecosystem depends on it.

The project also refreshed its Technical Committee, adding eight new members including Blake Embrey, Ulises Gascón, and Wes Todd. That group works under the "Express Forward Plan," a strategy for managing dependencies and engine support transparently.

Security became a top priority. The 2024 security audit led to adopting the OSSF Scorecard for ongoing health monitoring, and a partnership with HeroDevs offers Never-Ending Support for organizations still running legacy versions. None of that is glamorous work. It is the kind of work that decides whether a dependency is still safe to build on five years from now.

Backed by the Sovereign Tech Fund, the 2025–2026 roadmap covers three things:

  • Automated releases: automating npm publishing to remove manual error.
  • Scoped packages: moving official modules under an @expressjs/ scope.
  • Deep optimizations: performance work in the core libraries, expected to deliver a faster, more scalable framework by mid-2026.

When should you choose Express — and when not?

Choose Express when you want a low-overhead foundation and real control over every layer of the middleware stack. It is the natural fit for high-throughput APIs, microservices, and the small intermediary services where a heavyweight framework only gets in the way. Those are the cases where I still reach for it first.

When to choose Express and when not: lightweight APIs needing fine-grained control set against a large team needing built-in conventions

Look elsewhere if you want a batteries-included framework with rigid conventions and a built-in ORM so a large team can move fast without designing an architecture first. Express hands you the decisions instead, and the responsibility that comes with them. If your team is not ready to own that, its flexibility will cost you more than it returns.

References

Share this article