Node.js Framework

Express

Express is the minimal, unopinionated Node.js web framework. A route, a function and a middleware chain — that is the whole mental model, and it still powers a huge share of the web.

beginner16 min readUpdated Sep 16, 2026
server.js
js
// server.js
import express from "express";

const app = express();
app.use(express.json());

app.get("/users/:id", async (req, res, next) => {
  try {
    const user = await db.user.findById(req.params.id);
    if (!user) return res.status(404).json({ error: "not_found" });
    res.json(user);
  } catch (err) {
    next(err);
  }
});

app.listen(3000);
Released
2010
Runs on
Node.js
Style
Minimal, unopinionated
Core idea
Middleware chain
Language
JavaScript / TypeScript
Version
5.x

Why it matters

Why Express is still everywhere

Routing without ceremony

Define endpoints with app.get, app.post and friends. Paths can be static, parameterised or pattern-matched, and handlers stay plain functions.

Middleware for everything

Authentication, logging, parsing and validation are just functions in a chain. Compose small pieces instead of configuring a large framework.

An enormous ecosystem

Thousands of middleware packages and years of answers mean almost any problem already has a small, well-tested solution.

The big picture

The three ideas behind Express

A route matches a URL, a handler receives the request and response, and middleware wraps everything in between.

Routing

Match

A method and a path select the handler that runs. Routes can be grouped into routers and mounted under a prefix.

Middleware

Compose

Functions receive (req, res, next) and either end the response or pass control down the chain.

Request & Response

Exchange

Thin wrappers over Node's HTTP objects — read input, set status and headers, and send a body.

At a glance

What you actually get

Routes

app.get("/posts/:id") matches a URL and runs a handler.

Middleware

app.use(fn) runs before or after routes, in registration order.

req and res

Read params, query and body; set status and send JSON or files.

Routers

express.Router() keeps large apps modular and mountable.

Error middleware

A four-argument function centralises error responses.

Security middleware

helmet, cors and rate limits drop in as one-liners.

A short history

From Sinatra-inspired toy to web standard

  1. 2010

    Express is born

    TJ Holowaychuk releases a small framework inspired by Ruby's Sinatra.

    10
  2. 2011

    Connect middleware

    Express absorbs Connect's middleware model, which defines its API for years.

    11
  3. 2014

    Express 4

    The router is rewritten and middleware is unbundled into separate packages.

    14
  4. 2016

    The open governance era

    IBM and StrongLoop stewardship keeps Express maintained as Node grows.

    16
  5. 2024

    Express 5

    A long-awaited major release brings async error handling and a modern baseline.

    24

The complete guide

Express: Everything you need to know

What is Express?

Express is a minimal web framework for Node.js. It does not decide your folder structure, your database or your architecture. It gives you three things and gets out of the way: a way to match URLs, a request and response object, and a middleware chain that ties them together.

That restraint is the point. Express arrived in 2010, when Node’s built-in http module required you to write routing and body parsing by hand. It made those tasks a few lines. Fifteen years later it is still the default choice for Node APIs, and its ideas are copied by nearly every framework that followed.

If you understand Express, you understand the shape of server-side JavaScript.

The request and response cycle

At its core, an Express app is a function that receives a request and sends a response. Everything else is convenience on top.

import express from "express";

const app = express();

app.get("/", (req, res) => {
  res.send("Hello, world");
});

The req object wraps Node’s incoming message: req.params, req.query, req.body and req.headers give you the input. The res object wraps the outgoing message: res.status(), res.json(), res.send() and res.set() shape the output. A handler ends the request by calling one of the res methods.

Routing

A route is a method, a path and one or more handlers. Paths can be static, parameterised with :name, or matched with patterns.

app.get("/posts", listPosts);
app.get("/posts/:id", getPost);
app.post("/posts", createPost);
app.put("/posts/:id", replacePost);
app.patch("/posts/:id", updatePost);
app.delete("/posts/:id", deletePost);

Route parameters arrive in req.params, and query strings in req.query.

app.get("/posts/:id", (req, res) => {
  const { id } = req.params;          // "/posts/42" -> "42"
  const { fields } = req.query;       // "?fields=title" -> "title"
  res.json({ id, fields });
});

You can also pass several handlers to one route. They run in order until one ends the response, which is how you attach per-route middleware like validation or authorisation.

app.post("/posts", requireAuth, validatePost, createPost);

Middleware: the one idea to master

Middleware is the heart of Express. It is any function that receives (req, res, next) and either ends the response or calls next() to continue.

function logger(req, res, next) {
  const start = Date.now();
  res.on("finish", () => {
    console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
  });
  next();
}

app.use(logger);

Middleware runs in the order it is registered. That single rule explains most Express behaviour: register the JSON parser before routes that read req.body, register authentication before protected routes, and register the 404 handler after every route.

There are three kinds worth knowing:

  • Application middlewareapp.use(fn) runs for every request.
  • Router middlewarerouter.use(fn) runs only for that router’s paths.
  • Route middlewareapp.get(path, fn, handler) runs only for that route.

A middleware that takes four arguments is an error handler and only runs when something calls next(err).

Routers keep large apps readable

As an app grows, a single file of routes becomes unmanageable. express.Router() lets you group related routes and mount them under a prefix.

// routes/posts.js
import { Router } from "express";

const router = Router();

router.get("/", listPosts);
router.post("/", createPost);

export default router;
// app.js
import posts from "./routes/posts.js";

app.use("/api/v1/posts", posts);

Now every file owns one resource, and the URL prefix lives in a single place. This is the structure that keeps Express projects from turning into a pile of endpoints.

Error handling

Errors should flow to one place. Call next(err) from anywhere and Express skips ahead to the first four-argument error handler.

app.get("/users/:id", async (req, res, next) => {
  try {
    const user = await db.user.findById(req.params.id);
    if (!user) return res.status(404).json({ error: "not_found" });
    res.json(user);
  } catch (err) {
    next(err);
  }
});
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status ?? 500).json({
    error: err.code ?? "internal_error",
    message: err.message,
  });
});

Express 5 forwards rejected promises to the error handler automatically, so async handlers no longer need a wrapper. Still, being explicit with try/catch makes intent obvious and keeps the code portable. Register a 404 handler after all routes so unmatched paths return a proper JSON error instead of Express’s default HTML page.

Reading the request body

Express does not parse bodies by default. Add the built-in parsers before the routes that need them.

app.use(express.json());                        // application/json
app.use(express.urlencoded({ extended: true })); // form posts

For file uploads, reach for multer. For richer validation, combine parsing with a schema library like Zod or Joi and reject bad input early, before it reaches your handlers.

Security and production basics

Express is deliberately bare, so a few middleware one-liners cover the essentials:

import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";

app.use(helmet());                 // sensible security headers
app.use(cors({ origin: "https://app.example.com" }));
app.use(rateLimit({ windowMs: 60_000, max: 100 }));

Also disable x-powered-by, keep dependencies updated, validate and cap request sizes, and never trust client input. These are the same concerns every backend has; Express simply gives you a small, well-known place to handle each one.

Testing

Because handlers are plain functions, Express is easy to test. supertest boots the app in-process and makes real HTTP assertions without opening a port.

import request from "supertest";
import app from "../app.js";

test("GET /posts returns a list", async () => {
  const res = await request(app).get("/posts").expect(200);
  expect(Array.isArray(res.body)).toBe(true);
});

Separate the app definition (app.js) from the server start (server.js) so tests can import the app without listening. That one split makes integration testing trivial.

Best practices

  • Keep route handlers thin; move logic into services and data access layers.
  • Split routes by feature with express.Router() and mount them under a version prefix.
  • Register middleware deliberately and in order: parsers, then auth, then routes, then 404, then errors.
  • Forward every error with next(err) and format responses in one place.
  • Validate and cap all input before it reaches business logic.
  • Use helmet, cors and rate limiting from day one.
  • Separate the app from the server so tests stay fast.

Common mistakes

  • Forgetting express.json() and wondering why req.body is undefined.
  • Dropping rejected promises in async handlers on Express 4.
  • Registering the 404 or error handler before the routes it should catch.
  • Growing a single routes file into hundreds of lines.
  • Returning 200 for errors instead of using status codes.
  • Trusting req.body without validation.
  • Mixing res.send, res.json and res.end unpredictably.

Where to go next

Express teaches the request, the response and the middleware chain — concepts you will reuse forever. If you want more speed and built-in validation, read the Fastify guide next. If you want structure and dependency injection for a large team, move to NestJS. For edge and serverless deployments, Hono offers the same style on a web-standards runtime. And if any of this felt fast, revisit the Node.js basics underneath it.

In practice

One app, four moving parts

Switch between the pieces to see how they fit together.

server.js
import express from "express";
import routes from "./routes.js";

const app = express();

app.use(express.json());
app.use("/api", routes);

app.listen(3000, () => console.log("http://localhost:3000"));

Handling async errors

Express 5 forwards rejected promises automatically, but being explicit is still clearer and works the same way in any version.

Prefer
router.get("/posts", async (req, res, next) => {
  try {
    const posts = await Post.find();
    res.json(posts);
  } catch (err) {
    next(err);
  }
});
Avoid
router.get("/posts", async (req, res) => {
  const posts = await Post.find();
  res.json(posts);
  // rejection is silently lost in Express 4
});

Ordering middleware

Middleware runs in registration order, so the parser and auth must be mounted before the routes that depend on them.

Prefer
app.use(express.json());
app.use(auth);
app.use("/api", routes);
app.use(notFound);
app.use(onError);
Avoid
app.use("/api", routes);
app.use(express.json());
app.use(auth);
// body is undefined inside routes

Trade-offs

Is Express the right default?

Express optimises for familiarity and simplicity. Know the cost before you scale it.

Strengths

  • Nothing to learn first

    The API is tiny, the docs are excellent, and almost every Node developer can read an Express app on day one.

  • The ecosystem is unmatched

    Authentication, uploads, sessions, validation and logging all have mature middleware with years of production use.

  • You stay in control

    There is no hidden structure. You choose the folder layout, the database and the patterns instead of adopting a framework's opinions.

Trade-offs

  • You build the conventions

    Without an agreed structure, large Express apps drift into inconsistent handlers. A clear layout matters more here than in opinionated frameworks.

  • Performance is good, not great

    Express is fast enough for most services, but Fastify and raw Node can handle noticeably more requests per second.

  • Async errors are easy to miss

    In Express 4 a rejected promise is silently dropped. Even on 5, a try/catch or next(err) keeps failures visible.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Express?

Our interactive tutorial walks you through Express step by step — with quizzes and real code you can run in the browser.