Express backend
The fourth stack, and the deliberate counterpoint to NestJS: same language, opposite
philosophy. No DI container, no decorators, no modules. Plain Express, functions, and a
composition root that wires everything by hand in main.ts. It passes the same 16
contract tests as the other three.
Putting Express and NestJS side by side is the whole point. Same domain, same use
cases, same adapters. The only real difference is how the framework is assembled:
NestJS resolves a dependency graph from decorators and tokens; Express is you, calling
new, in order, in one file. Neither is wrong. The recipes compare them honestly.
Layout
src/domain types, enums, errors. Imports nothing framework-y.
src/usecase ports (plain interfaces) + services.ts (all use cases)
src/adapter postgres (pg), redis (ioredis), broker (amqplib), paymentgw (fetch), memory
src/platform clock, uuid, bcrypt, JWT token service
src/transport dto mappers + http.ts (the whole Express app: middleware, routes, error envelope)
src/main.ts composition root: hand-wires adapters into use cases, starts sweeper + worker
The dependency arrow is identical to every other backend: transport and adapters depend on use cases, use cases depend on the domain, the domain depends on nothing. Here the wiring is just visible in one file instead of inferred by a container.
Run it
# set COMPOSE_PROFILES=express in .env, then:
make up
# or one-off:
COMPOSE_PROFILES=express docker compose up -d --build
Test it
cd backends/express
npm install
npm test # unit tests, no infrastructure
cd ../../contract/tests
TARGET_URL=https://localhost/api pytest -v # shared contract suite
Notes
- Async errors. Express 5 forwards rejected promises from async handlers to the
error middleware automatically (Express 4 did not). We keep a small
wrap()helper anyway, as belt-and-suspenders and to make the error path explicit for readers. - Raw body for webhooks.
express.jsonis configured with averifyhook that stashesrawBody, which the webhook route needs for HMAC verification. - Lenient UUID check. Like the NestJS backend, path/cursor ids are validated with a
plain 8-4-4-4-12 hex regex, not the
uuidpackage's strict RFC 4122validate(), which would reject the lab's tidy seed ids.