const express = require("express");
const app = express();
app.get("/orders/:orderId", (req, res) => {
const { orderId } = req.params; // path segment, extracted automatically
res.json({ orderId });
});
app.get("/orders", (req, res) => {
const { status } = req.query; // query string: /orders?status=shipped
res.json({ filteringBy: status ?? "all" });
});
req.params holds named path segments (:orderId), and req.query holds parsed query-string parameters — Express handles the parsing for both, instead of you slicing req.url apart yourself.
Express middleware is a function of (req, res, next). Call next() to hand control to the next middleware or route handler in the chain; don't call it, and the chain simply stops there — the standard way to end the pipeline is to also send a response yourself (res.json(...), res.send(...)), otherwise the request just hangs with no response and no error.
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next(); // without this, every route below would never run
});
app.use(express.json()); // parses JSON bodies into req.body, for every route below this line
app.get("/health", (req, res) => res.json({ ok: true }));
Registration order is execution order — app.use(express.json()) only applies to routes declared after it, which is exactly why body-parsing and logging middleware are conventionally registered right at the top, before any routes.
Express distinguishes an error handler from a normal middleware purely by counting its parameters: a function with exactly four (err, req, res, next) is treated as error-handling middleware, and Express only invokes it when something calls next(err) — passing any value to next() — or a synchronous handler throws.
app.get("/orders/:orderId", (req, res, next) => {
try {
const order = findOrder(req.params.orderId);
if (!order) {
const err = new Error("Order not found");
err.status = 404;
return next(err); // hands off to the error-handling middleware below
}
res.json(order);
} catch (err) {
next(err);
}
});
// Must be registered LAST — Express matches error handlers by arity, and
// route handlers registered after this would never be reached on an error path anyway
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({ error: err.message || "Internal server error" });
});
It has to come last for a structural reason, not just a style convention: Express walks the middleware stack top to bottom, and once next(err) is called, it skips every remaining normal middleware, searching forward for the next error-handling one. If your error handler is registered above some of your routes, an error thrown in one of those routes has nowhere earlier to be caught — it'll fall through to Express's built-in default handler (a bare stack trace) instead of yours.