Building a Production REST API with Node.js and Express in 2026
Node.jsBackendAPI

Building a Production REST API with Node.js and Express in 2026

Layered architecture, validation, error handling, auth, rate limiting, observability — the patterns I use to ship Node.js + Express APIs that don't fall over in production.

HJ
Hassan Javed
April 2026
12 min read

Why Express, in 2026

I keep getting asked: "Express? Still? Why not Hono / Fastify / Bun?"

Express is still the most common Node.js HTTP framework I see in production codebases, by a wide margin. It's not the fastest, it's not the most modern, but it's stable, mature, and every Node engineer knows it. For a senior who needs to onboard a junior in two days, Express wins.

That said, I'll mention where Fastify or Hono make sense at the end. The patterns in this post apply to all three; Express is just the example.

The architecture I keep coming back to

Five layers, each with one responsibility:

routes/ — HTTP layer (Express routers, validation)
controllers/ — Glue (HTTP → service)
services/ — Business logic (no HTTP, no DB-specific code)
repositories/ — Data layer (DB queries only)
lib/ — Shared utilities (logger, config, errors)

A request flows: route → controller → service → repository → DB → service → controller → response.

The reason for this separation is testability. Services don't know about HTTP, so you can unit-test business logic without booting Express. Repositories don't know about services, so you can swap MongoDB for PostgreSQL without touching business logic.

Validation: at the boundary, not throughout

Bad code I see all the time has validation scattered across the service. Better: validate at the route boundary with Zod, so your service receives already-validated data and stays clean.

I use Zod everywhere now. Joi was the standard for years; Zod is faster, has better TypeScript inference, and the schema syntax is more readable.

Error handling that doesn't suck

The Express default — a single next(err) and a global handler — is fine but easy to misuse. The pattern:

1.Custom error classes for known cases (HttpError, NotFoundError, ValidationError)
2.Throw them anywhereif (!user) throw new NotFoundError("User")
3.Single error middleware at the bottom that translates known errors to HTTP responses and logs unknown errors

The benefit: known errors have clear codes. Unknown errors get logged with context and a generic message goes back to the client. No leaking stack traces in production.

Authentication: JWT with refresh, not session cookies

For a stateless REST API, JWT with a short-lived access token (15 min) and a long-lived refresh token (7 days) stored in an HttpOnly cookie is the pattern I default to.

Access token: signed JWT, 15-minute expiry, sent in Authorization: Bearer header
Refresh token: random opaque string, stored in DB, sent in HttpOnly cookie
Refresh endpoint: validates the refresh token against DB, issues a new access token, optionally rotates the refresh token

When NOT to use this: if you only need browser clients on a single domain, a simple HttpOnly session cookie is simpler and more secure (no JWT footguns). JWT shines when you have multiple clients (web + mobile + third-party API consumers).

Rate limiting: at multiple layers

A single layer of rate limiting is brittle. I use three:

1.At the edge (Cloudflare or your CDN): per-IP basic protection against floods
2.At the app (express-rate-limit or rate-limit-redis): per-user, per-route limits
3.At sensitive operations (login, password reset, payment): aggressive limits with progressive delays

Don't roll your own — express-rate-limit with a Redis store handles 99% of cases.

Observability: logs, metrics, traces

The minimum viable observability stack for a Node API:

1.Structured logs with Pino, going to stdout. Don't log to files in containerized deployments.
2.Metrics with prom-client exposing /metrics. Counters for requests by route + status, histograms for latency.
3.Traces with OpenTelemetry. The auto-instrumentation for Express + your DB driver gets you 80% of the value with one initialization line.

Where to ship the data: Grafana Cloud (free tier is enough for most projects), Datadog (expensive but excellent), or Honeycomb (best for tracing).

The DB layer

For new projects in 2026 I'm using:

PostgreSQL + Drizzle ORM for SaaS apps where I want SQL and types
MongoDB + native driver for projects with flexible schemas (a lot of Web3 indexing)
Redis for caching, rate limiting, sessions

Mongoose is fine but Drizzle's type inference is better than Mongoose's schemas, and for SQL-relational data, Postgres is the right answer.

When to reach for Fastify or Hono instead

Fastify: if you're benchmarking and Express's overhead matters (rare — most APIs are I/O-bound by DB calls). Fastify is 2-3x faster than Express for tight benchmarks.
Hono: if you're deploying to edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy). Hono is built for this and Express isn't.
Next.js Route Handlers: if you don't need a separate API service. App Router's route handlers cover what most SaaS APIs need.

What I ship for clients

A typical Node.js + Express API engagement looks like:

Express + TypeScript
Zod for validation
Pino for logging
prom-client + OpenTelemetry for observability
PostgreSQL + Drizzle (or MongoDB native, depending on schema needs)
JWT auth with refresh tokens
Rate limiting at app + sensitive-route levels
Dockerfile + GitHub Actions CI for deployment
README with setup, env vars, runbook

If you're starting a new Node.js + Express project and want a senior to architect it, reach out — backend engagements are part of my core service offering.

TL;DR

Layered architecture: route → controller → service → repository
Validate at the boundary with Zod, throw custom HttpError classes, single global error middleware
JWT + refresh tokens for multi-client APIs; HttpOnly session cookie for single-client
Rate limit at edge + app + sensitive operations
Pino + prom-client + OpenTelemetry for observability
Postgres + Drizzle or MongoDB native for the DB
Related Reads

You might also like