Skip to content
Node.js core 3 min read

REST APIs with Express

Express is the most widely used Node web framework. It adds a clean routing layer, a powerful middleware pipeline, and conveniences for parsing requests and shaping responses—all while staying minimal and unopinionated. This page builds a complete CRUD REST API from scratch.

Setup

Install Express and create an app:

npm install express
import express from "express";

const app = express();
app.use(express.json()); // parse JSON request bodies

app.get("/", (req, res) => {
  res.json({ status: "ok" });
});

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

express.json() is built-in body-parsing middleware that populates req.body from a JSON payload—something you had to do manually with the raw http module.

Routing

Express maps HTTP methods and paths to handlers. Each handler receives req and res.

app.get("/users", listUsers);
app.post("/users", createUser);
app.get("/users/:id", getUser);
app.put("/users/:id", updateUser);
app.delete("/users/:id", deleteUser);

For larger apps, group related routes with express.Router() and mount them under a prefix.

Middleware

Middleware are functions that run in order on every matching request. They receive (req, res, next) and either respond or call next() to pass control along.

function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();
}

app.use(logger);

The middleware pipeline is Express’s core idea: authentication, logging, parsing, and error handling are all just middleware composed in sequence.

Reading params, query, and body

SourceAccessExample URL
Route paramsreq.params.id/users/42
Query stringreq.query.sort/users?sort=name
Request bodyreq.bodyJSON in a POST
app.get("/users/:id", (req, res) => {
  const { id } = req.params;
  const { fields } = req.query;
  res.json({ id, fields });
});

A full CRUD REST API

Here’s a complete in-memory resource with all five operations and correct status codes:

import express from "express";

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

let users = [{ id: 1, name: "Ada" }];
let nextId = 2;

// List
app.get("/users", (req, res) => {
  res.json(users);
});

// Read one
app.get("/users/: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);
});

// Create
app.post("/users", (req, res) => {
  if (!req.body.name) {
    return res.status(400).json({ error: "name is required" });
  }
  const user = { id: nextId++, name: req.body.name };
  users.push(user);
  res.status(201).json(user);
});

// Update
app.put("/users/:id", (req, res) => {
  const user = users.find((u) => u.id === Number(req.params.id));
  if (!user) return res.status(404).json({ error: "Not found" });
  user.name = req.body.name ?? user.name;
  res.json(user);
});

// Delete
app.delete("/users/:id", (req, res) => {
  users = users.filter((u) => u.id !== Number(req.params.id));
  res.status(204).end();
});

app.listen(3000);

Create a user with curl:

curl -X POST localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Grace"}'

Output:

{"id":2,"name":"Grace"}

Error-handling middleware

Express recognizes error handlers by their four arguments: (err, req, res, next). Register it last so it catches errors from every route.

// Catch-all for unknown routes
app.use((req, res) => res.status(404).json({ error: "Route not found" }));

// Central error handler — must have 4 params
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({ error: err.message });
});

In Express 5, errors thrown in async handlers are forwarded to this handler automatically. In Express 4, wrap async routes or call next(err) yourself.

Best Practices

  • Always register express.json() before routes that read bodies.
  • Validate input early and return 400 with a clear message.
  • Centralize errors in one error-handling middleware; never leak stack traces in production.
  • Organize routes with express.Router() and keep handlers thin—push logic into services.
  • Use middleware for cross-cutting concerns: auth, rate limiting, CORS, logging.
  • Send accurate status codes—201 on create, 204 on delete, 404 when missing.
Last updated June 1, 2026
Was this helpful?