Skip to content
Node.js nd libraries 5 min read

Express.js Overview

Express is the most widely used web framework for Node.js — a thin layer over the built-in node:http module that adds routing, middleware, and convenient request/response helpers without imposing a particular project structure. It is deliberately minimal and unopinionated: it gives you primitives for handling HTTP and stays out of the way for everything else, which is why it underpins countless APIs, tooling backends, and full-stack frameworks. This page walks through installation, routing, middleware, the req/res helpers, and assembling a small REST API with the Router.

Installing and a first server

Express runs on any maintained Node.js release; Node 20 or 22 LTS is the sensible default. Express 5 (the current major) is published as plain CommonJS but works seamlessly from ES modules via Node’s interop.

npm init -y
npm install express

A minimal server is just an app, a route, and a listen call. Mark your package.json with "type": "module" to use the import syntax below.

import express from "express";

const app = express();

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

app.listen(3000, () => {
  console.log("Server listening on http://localhost:3000");
});

Output:

Server listening on http://localhost:3000

In CommonJS the only difference is the import line: const express = require("express");.

Routing

A route binds an HTTP method and a URL path to a handler function. Express exposes a method for each verb — app.get, app.post, app.put, app.patch, app.delete — plus app.all for every verb. Paths can contain named parameters (:id) that are parsed into req.params, and the query string is parsed into req.query.

// GET /users/42?fields=name
app.get("/users/:id", (req, res) => {
  res.json({
    id: req.params.id,        // "42"
    fields: req.query.fields, // "name"
  });
});

app.post("/users", (req, res) => {
  res.status(201).json({ created: true });
});

Handlers run in the order routes are registered, and the first one whose method and path match wins. A handler can also pass control to the next matching route by calling next().

PropertySourceExample
req.paramsNamed path segments/users/:idreq.params.id
req.queryParsed query string?page=2req.query.page
req.bodyParsed request bodyrequires a body parser (see below)
req.headersRequest headersreq.headers["content-type"]

Middleware

Middleware is the core idea in Express. A middleware function receives (req, res, next), can read or mutate the request and response, and then either ends the response or calls next() to pass control down the chain. Routing, body parsing, logging, authentication, and error handling are all just middleware. You register it with app.use, optionally scoped to a path prefix.

// Application-level middleware: runs on every request
app.use((req, res, next) => {
  req.startedAt = Date.now();
  console.log(`${req.method} ${req.url}`);
  next();
});

// Built-in body parser for JSON request bodies
app.use(express.json());

// Path-scoped middleware: only for /admin/*
app.use("/admin", (req, res, next) => {
  if (req.headers.authorization !== "Bearer secret") {
    return res.status(401).json({ error: "Unauthorized" });
  }
  next();
});

Express 5 ships express.json(), express.urlencoded(), and express.static() built in — there is no longer a need for the separate body-parser package. Third-party middleware (CORS, compression, helmet) is installed from npm and mounted the same way.

Order matters. Middleware runs top-to-bottom, so register body parsers and CORS before the routes that depend on them, and register error handlers last.

Error-handling middleware

An error handler is distinguished by its four arguments: (err, req, res, next). Express routes errors to it whenever a synchronous handler throws or — in Express 5 — when an async handler rejects. Define it after all your routes.

app.get("/boom", (req, res) => {
  throw new Error("something broke");
});

// Centralized error handler (note the 4 args)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: "Internal Server Error" });
});

Request and response helpers

On top of the raw Node streams, Express decorates req and res with helpers that cover the common cases concisely.

app.get("/demo", (req, res) => {
  res.status(200);                 // set status code
  res.set("X-Powered-By", "demo"); // set a header
  res.cookie("sid", "abc123");     // set a cookie
  res.json({ ok: true });          // send JSON + content-type
});

// Other common responses:
// res.send("text or buffer")
// res.redirect("/login")
// res.sendFile("/abs/path/report.pdf")
// res.sendStatus(204)

res.json() serializes an object and sets Content-Type: application/json; res.send() infers the type from the argument; and res.redirect() issues a 302 by default. Each helper returns res, so they chain.

Building a REST API with the Router

For anything beyond a toy app, group related routes into a Router — a mini-application you can mount under a path prefix. This keeps each resource in its own file and keeps your entry point small.

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

const router = Router();
const users = [{ id: 1, name: "Ada" }];

router.get("/", (req, res) => res.json(users));

router.get("/:id", (req, res) => {
  const user = users.find((u) => u.id === Number(req.params.id));
  if (!user) return res.status(404).json({ error: "Not found" });
  res.json(user);
});

router.post("/", (req, res) => {
  const user = { id: users.length + 1, name: req.body.name };
  users.push(user);
  res.status(201).json(user);
});

export default router;
// app.js
import express from "express";
import usersRouter from "./routes/users.js";

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

app.use("/api/users", usersRouter); // mounted under a prefix

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

A GET /api/users/1 now resolves to the router’s /:id handler. Routers compose: a router can mount another router, letting you build nested resource trees.

Output (curl -s localhost:3000/api/users/1):

{"id":1,"name":"Ada"}

Best Practices

  • Keep the entry file thin: split each resource into its own Router module and mount it under a clear prefix like /api/users.
  • Register middleware in the right order — body parsers and CORS first, routes next, the error handler last.
  • Always send a response or call next() from every handler; a path that does neither leaves the request hanging.
  • Use a single centralized error-handling middleware instead of duplicating try/catch and status logic across routes.
  • Validate and sanitize req.body, req.params, and req.query with a schema library before trusting them.
  • Set security and performance middleware (helmet, compression, rate limiting) once at the app level rather than per route.
  • On Express 4, wrap async handlers so rejected promises reach your error handler; Express 5 forwards them automatically.
Last updated June 14, 2026
Was this helpful?