Web Development

Express middleware patterns we reuse across every project

DEV

After building enough Express APIs, a handful of small middleware functions have become nearly universal across our projects. Rather than let each new project reinvent them slightly differently, we have started treating them as a small internal standard, and it is worth writing down what they actually do and why each one exists.

The request logger

A request logger that captures method, path, status code, and response time for every request, written as structured JSON rather than a plain formatted string, so it can be piped into whatever log aggregation tool a given client's infrastructure already uses. Early versions of this logged to plain text, which worked fine until we needed to actually query logs for "show me every request over 500ms in the last hour" and discovered that grepping formatted text for that is far more painful than querying structured fields would be.

The centralized error handler

A centralized error handler that catches anything thrown or rejected further down the stack and formats a consistent JSON error response instead of letting Express's default HTML error page leak through to what is very often a JSON-consuming API client that has no use for an HTML stack trace page. This middleware sits last in the chain, as Express's error-handling convention requires, and distinguishes between operational errors we expect and want to surface with a specific message (a validation failure, a not-found resource) and unexpected errors, which get a generic message to the client while the actual details go to our error tracking service rather than being exposed to whoever happens to be calling the API.

The async wrapper

A simple async wrapper that lets us write route handlers with `async/await` without a `try/catch` block in every single one. Express's routing does not natively understand promises the way some newer frameworks do, so an unhandled rejection inside an `async` route handler would previously either crash the process or silently swallow the error depending on Node version and configuration, neither of which is acceptable. The wrapper is a small higher-order function that catches any rejection and forwards it to Express's `next()`, letting the centralized error handler described above deal with it consistently rather than each route handler needing its own boilerplate.

A request-ID middleware we added more recently

  • Generates a unique ID for every incoming request, attaching it to the request object and to the response headers
  • Gets included in every log line the request logger writes for that request, letting us trace a single request's full lifecycle through logs even when several requests are being handled concurrently
  • Gets forwarded to any downstream service calls the request triggers, so a request that spans multiple services can still be traced end to end by that one ID

Why writing this down as a starter repository mattered more than the code itself

None of these are complicated individually, but writing them fresh for every project wastes time and introduces inconsistency — a subtle difference in how one project's error handler formats a validation error versus another's has caused confusion more than once when a developer moved between projects and carried the wrong mental model with them. We keep them in a small internal starter repository now, and a new Express project begins with these already wired in rather than reinvented, which has cut a genuinely tedious afternoon of boilerplate off the start of every new project and, more importantly, means a developer moving between our client projects encounters the same conventions everywhere rather than needing to relearn subtly different approaches to the same basic problems each time.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients