CodeOath
← All posts
Node.js36 min total · 14 parts

Node.js Fundamentals: The Runtime, the Event Loop, and Building Real APIs

Part 6 of 14 · ~2 min

Express Fundamentals

Routing and route parameters

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.

The middleware pattern

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.

Error-handling middleware: the four-argument signature

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.