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
| Source | Access | Example URL |
|---|---|---|
| Route params | req.params.id | /users/42 |
| Query string | req.query.sort | /users?sort=name |
| Request body | req.body | JSON 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
asynchandlers are forwarded to this handler automatically. In Express 4, wrap async routes or callnext(err)yourself.
Best Practices
- Always register
express.json()before routes that read bodies. - Validate input early and return
400with 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—
201on create,204on delete,404when missing.