Express Clean Backend
Production-ready Node.js/TypeScript template: strict hexagonal architecture, Keycloak OIDC, CQRS-lite, S3, Traefik 3.7 with ACME, and a Loki/Grafana/Prometheus stack.
Node.js · TypeScript · Hexagonal Architecture · Clean Architecture

Overview
A Node 22 + TypeScript backend meant to be the starting point for a production API. It is not a "Hello World with auth" boilerplate: identity is delegated to Keycloak, repositories are split CQRS-lite style, auditing is decoupled through events, S3 storage sits behind a port, and the full logging and metrics stack runs behind Traefik with automatic HTTPS.
The API is deployed at api.jmrg.dev/api-docs, with the readiness endpoint at api.jmrg.dev/health/ready.
Why it exists
Every time I started a Node project from scratch I ended up copying the same four modules: the Keycloak wrapper, the logger with correlation, the error handler with its chain, and the docker-compose file with Traefik. This template consolidates all of that into one repository with strict hexagonal architecture and rules that can be checked, so those decisions are not made by hand again on every new project.
Decisions worth explaining
Keycloak as the only IdP. No proprietary JWTs. The API validates RS256 against the JWKS with jose. Human-facing panels (Grafana, Traefik, Prometheus) are protected by Traefik's OIDC plugin pointing at the same realm, so there are not three separate logins for three services.
Separate read and write repositories. UserQueryRepository and UserCommandRepository do not share an interface. That keeps a findById from dragging mutation methods along with it, and leaves the door open to move to full CQRS later without rewriting the domain.
Auditing through Observer. LoginUseCase publishes events on an EventBusPort and AuditLoginObserver persists them. The use case does not know auditing exists.
Storage behind a port. StoragePort with S3StorageAdapter for real AWS (or LocalStack in development) and FakeStorageAdapter for tests. The adapter guards against path traversal, because someone will try it.
Logging with context. Winston is decorated with RequestContextLoggerDecorator, which propagates the correlationId through AsyncLocalStorage. No need to thread it as a parameter through every function.
Observability provisioned up front. Loki, Prometheus and Grafana ship with dashboards and datasources as JSON. It comes up working, with no dashboards to click together by hand.
Automatic HTTPS. Traefik 3.7 issues the certificate through Let's Encrypt on first boot using the HTTP-01 challenge, and renews it from then on.
Architecture
Four layers, with dependencies pointing inward:
src/
├── domain/ # entities, value objects, ports
├── application/ # use cases, one per feature
├── infrastructure/ # adapters: keycloak, mongo, s3, winston, events
└── presentation/ # Express routers + middleware + bootstrapThe rule is simple: nothing inside domain/ or application/ may import mongoose, express, aws-sdk, keycloak-connect, jose or winston. ESLint enforces it in CI.
Stack
| Layer | Technology |
|---|---|
| Runtime | Node ≥ 22, TypeScript strict, native ESM |
| HTTP | Express 5 with helmet, cors, express-rate-limit |
| Auth | Keycloak 26.6 (OIDC) + jose (JWKS RS256) |
| Persistence | MongoDB 8.2 + Mongoose |
| Storage | AWS S3, LocalStack in development |
| Validation | Zod, fail-fast per subsystem |
| Logs | Winston → Loki 3.7 |
| Metrics | Prometheus 3.8 + Grafana 13 |
| Reverse proxy | Traefik 3.7 + sevensolutions/traefik-oidc-auth plugin |
| Testing | Vitest + supertest + mongodb-memory-server |
| Package manager | pnpm |
Design patterns
| Category | Pattern | Where it shows up |
|---|---|---|
| Structural | Hexagonal (Ports & Adapters) | domain/<feature>/port ← infrastructure/<feature>/adapter |
| Structural | Adapter | KeycloakAdapter, S3StorageAdapter, WinstonLoggerAdapter |
| Structural | Facade | UserFacade, AuthFacade, StorageFacade |
| Structural | Decorator | RequestContextLoggerDecorator |
| Behavioural | Chain of Responsibility | Error handler: Zod → Client → Server → Mongo → Fallback |
| Behavioural | Observer | EventBusPort + AuditLoginObserver |
| Behavioural | Strategy | Format selector in WinstonLoggerAdapter |
| Behavioural | Template Method | ErrorHandler.handle / delegate |
| Creational | Factory Method | CustomError.notFound, .conflict, .unauthorized… |
| Creational | Singleton | MongoDatabase, S3Client |
| DDD | Repository, CQRS-lite | UserQueryRepository + UserCommandRepository |
Endpoints
| Method | Endpoint | Auth | Roles |
|---|---|---|---|
| POST | /api/v1/auth/register |
public, rate limit 3/min/IP | — |
| POST | /api/v1/auth/login |
public, rate limit 5/min | — |
| POST | /api/v1/auth/refresh |
public | — |
| GET | /api/v1/auth/me |
Bearer | any |
| GET | /api/v1/users |
Bearer | admin |
| POST | /api/v1/users |
Bearer | admin |
| PUT | /api/v1/users/:id |
Bearer | admin |
| DELETE | /api/v1/users/:id |
Bearer | admin |
| GET | /api/v1/storage/objects |
Bearer | admin |
| GET | /api/v1/storage/signed-url |
Bearer | admin |
| GET | /health/ready |
public | — |
| GET | /metrics |
LAN only | — |
Full OpenAPI spec at api.jmrg.dev/api-docs.
Deployment
In development, two commands:
docker compose --profile dev up -d # base + LocalStack
docker compose --profile dev --profile observability up -d # + Loki/Grafana/Prom
pnpm dev # API on the host with tsx watchFor production, define .env.production:
DOMAIN=yourdomain.com
SSL_EMAIL=[email protected]
KEYCLOAK_INTERNAL_URL=https://auth.yourdomain.comThen start it:
./scripts/init-acme.sh
docker compose --env-file .env.production up -dTraefik issues the Let's Encrypt certificate on first boot and renews it automatically.
Documentation
14 chapters under docs/, in four blocks:
- Project — structure, architecture, features, security, configuration, testing, contributing.
- API — endpoints, Zod schemas, error chain.
- Infrastructure — Docker, Traefik, ACME, observability.
- Operations — runbook, health checks, troubleshooting.
Links
- Repo: github.com/jmrg-link/express_clean_code_template
- API: api.jmrg.dev/api-docs
- Health: api.jmrg.dev/health/ready
- License: MIT