Skip to contentJMRG
All projects
COMPLETED

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 + bootstrap

The 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>/portinfrastructure/<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 watch

For production, define .env.production:

DOMAIN=yourdomain.com
SSL_EMAIL=[email protected]
KEYCLOAK_INTERNAL_URL=https://auth.yourdomain.com

Then start it:

./scripts/init-acme.sh
docker compose --env-file .env.production up -d

Traefik 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.