Skip to content
Community content. Review instructions before giving them to an AI agent — treat modules like open-source code.

Node Express Conventions

Express API conventions: router/controller/service layering, centralized error middleware, async handler wrapping, edge validation, env-based config, graceful shutdown, and keeping routes free of business logic.

Mby @markdownersPublished August 21, 2026 · ~3 min read

0 downloads · Used by 0 stacks

Layer every Express app the same way — router defines the URL, controller translates HTTP to a call, service holds the logic — and let a single centralized error handler be the only place that turns a thrown error into an HTTP response.

Router / controller / service layering

  • Keep route files (router.get(...)) responsible only for wiring a path and HTTP method to a controller function — no business logic, no direct database calls in a route file.
  • Keep controllers responsible for translating the HTTP request into a call to a service function and the service's return value into an HTTP response (status code, JSON shape) — a controller should not contain domain logic or direct query building.
  • Keep services responsible for the actual business logic and data access, with no knowledge of req/res — a service function should be callable from a controller, a queue worker, or a test with the same signature.
  • Never let a route handler directly query the database or call a third-party API inline "just this once" — that logic becomes unreusable and untestable the moment it's needed elsewhere.

Centralized error handling

  • Register exactly one error-handling middleware (four-argument signature) at the end of the middleware chain, and have every other layer throw or next(err) rather than manually formatting an error response inline.
  • Distinguish operational errors (expected: not-found, validation failure, unauthorized) from programmer errors (bugs) with a typed/known error class, and map known error types to their correct status codes in the central handler — don't let every error fall through to a generic 500.
  • Never leak stack traces or internal error details to the client in production; log the full error server-side and return a minimal, safe message to the caller.

Async handler wrapping

  • Wrap every async route handler (or use a framework version that does this natively) so a rejected promise is forwarded to next(err) automatically — an unwrapped async handler that throws will hang the request or crash the process instead of hitting the error middleware.
  • Never mix callback-style and promise-style error handling in the same handler; pick async/await consistently and let the wrapper handle rejections.

Validation at the edge

  • Validate every request's body, query, and params against a schema at the edge (middleware or the top of the controller) before any of it reaches a service — services should be able to trust the shape of their inputs.
  • Return a 400 with a clear, structured validation error on failure, and stop processing immediately — never let a partially-invalid payload continue deeper into the request lifecycle.
  • Re-validate on the server even when the client also validates — client-side validation is UX only, never a security or correctness boundary.

Configuration from environment

  • Load all configuration (ports, database URLs, secrets, feature flags) from environment variables through a single validated config module read once at startup, not scattered process.env.X reads throughout the codebase.
  • Fail fast at startup if a required environment variable is missing, rather than discovering it when the first request that needs it arrives.
  • Never commit real secrets to a .env file in version control; commit a .env.example with variable names and placeholder values instead.

Graceful shutdown

  • Listen for termination signals (SIGTERM, SIGINT) and stop accepting new connections, finish in-flight requests, close database/queue connections, then exit — an abrupt process kill drops in-flight requests and can leave connections in a bad state.
  • Set a hard timeout on graceful shutdown (e.g., a few seconds) so a hung connection can't block deployment/restart indefinitely; force-exit after the timeout.

Keep routes free of business logic

  • Treat "can I unit-test this without spinning up an HTTP server" as the litmus test for whether logic has leaked into the router/controller layer — if a piece of logic requires mocking req/res to test, it belongs in a service instead.
  • Resist adding "just a quick if-check" inline in a route for a one-off business rule; add it to the relevant service function so the rule lives with the rest of that domain's logic.
Badge

Link back to this module from your own README.

Get it on Markdowners
[![Get it on Markdowners](https://markdowners.com/mdstack-badge.svg)](https://markdowners.com/m/markdowners/node-express-conventions)

Comments (0)

Sign in to comment. Sign in

No comments yet. Be the first to add one.

Discussions about this module

No discussions about this module yet.

Start a discussion