Phase 2 · Web Servers & HTTP APIsModule 13~58 min read

Middleware, Validation & Composition

Build an intentional Express pipeline for request IDs, logging, validation, authorization boundaries, and reusable route behavior.

What you'll learn

Middleware turns cross-cutting HTTP behavior into an explicit pipeline. You will compose request identity, timing, validation, and authorization boundaries without hiding domain rules or losing control flow.

By the end of this lesson, you'll be able to:

  • Trace execution through the middleware stack
  • Create request-scoped context and timing
  • Build reusable validation middleware
  • Place authentication and authorization at clear boundaries

Core mental model

Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.

ConceptWhat it meansDecision rule
MiddlewareA function that observes or changes the request-response cycleUse for cross-cutting transport concerns, not core domain rules
next()Transfers control to the next matching layerCall once only when this middleware has not ended the response
Request contextSafe per-request metadata such as request ID and principalAttach namespaced values early and avoid global mutable state
Middleware factoryConfiguration creates a reusable middleware functionUse for parameterized validation and authorization policies

Professional workflow

Build and verify Node.js programs from the terminal in small, observable steps.

  1. List cross-cutting concerns and their required order
  2. Assign a request ID before logging
  3. Parse before validating
  4. Authenticate before authorization
  5. Store validated input separately from raw input
  6. Mount resource routes
  7. Handle not-found and errors last
  8. Test short-circuit and next paths

Keep the feedback loop short

Run the smallest useful command after every meaningful change. Read the complete error message before editing again, and keep inputs and outputs visible while you learn.

Guided code lab

Add request identity and completion logging

finish observes the completed response. The request ID connects inbound, application, and error logs.

request-context.js
import { randomUUID } from "node:crypto";

export function requestContext(request, response, next) {
  request.context = { requestId: request.get("x-request-id") ?? randomUUID() };
  response.set("x-request-id", request.context.requestId);
  const started = performance.now();

  response.on("finish", () => {
    console.log(JSON.stringify({
      requestId: request.context.requestId,
      method: request.method,
      path: request.originalUrl,
      status: response.statusCode,
      durationMs: Math.round(performance.now() - started),
    }));
  });
  next();
}

Build a validation middleware factory

The validator returns normalized data or issues. Downstream handlers consume validatedBody rather than reparsing raw input.

validate.js
export function validateBody(validator) {
  return function validationMiddleware(request, response, next) {
    const result = validator(request.body);
    if (!result.ok) {
      response.status(422).json({
        error: { code: "VALIDATION_FAILED", issues: result.issues },
      });
      return;
    }
    request.validatedBody = result.value;
    next();
  };
}

Compose the pipeline intentionally

Order is behavior. Each layer receives the guarantees created by earlier layers.

app.js
app.use(requestContext);
app.use(express.json({ limit: "100kb" }));

app.post(
  "/api/tasks",
  requireAuthentication,
  requirePermission("tasks:create"),
  validateBody(validateNewTask),
  createTaskHandler,
);

app.use(notFoundHandler);
app.use(errorHandler);

Production practice

Make order visible

Keep top-level registration readable as a pipeline; avoid modules that secretly register global middleware.

Short-circuit clearly

After sending a rejection response, return instead of calling next or continuing execution.

Keep policies focused

Authentication establishes identity; authorization decides whether that identity may perform this operation.

Common failure mode

Calling next() after sending a response lets later middleware attempt another response, while forgetting next() on the success path leaves the request hanging.

Independent workshop

Add a production-shaped middleware pipeline to the project API.

Your finished workshop must include:

  • Request IDs echoed in responses
  • Completion logs with status and duration
  • Reusable body and parameter validators
  • Separate authentication and permission checks
  • Tests proving both short-circuit and success ordering

Definition of done

Run the happy path and at least two edge cases, keep responsibilities separated, and add a short README explaining how to run the program.

Recap & quick check

Key takeaways

  • Middleware order changes behavior
  • next transfers control
  • Cross-cutting transport concerns fit middleware
  • Validated data should be distinct from raw input
  • Authentication and authorization are different boundaries
  • Request context must be request-scoped

Quick check

1. What must middleware do if it does not end the response?

2. Which normally comes first?

3. Where should a request ID be stored?

4. What should happen after middleware sends a 422 response?

Next: Configuration, Errors & Observability