Wiring an application
users-api registers the packages once, in three modules. A handler then only declares the behaviors specific to it, as in A command through every layer.
The order a request takes
Section titled “The order a request takes”HttpCorrelationMiddlewaresets the correlation id;TenantSchemaMiddlewareruns the rest of the request inside the tenant named byx-tenant-schema(see Several tenants in one deployment).AuthSessionGuardresolves the principal;SessionPrincipalContextInterceptormakes it available to the application;HttpRouteInterceptornames the HTTP span by its route.StandardSchemaValidationPipevalidates the parameters declared with aschema.- The controller dispatches a command or query; the bus runs its pipeline: the global
beforebehaviors, the handler’s own, then the handler. - On the way out, the exception filters turn the packages’ framework-neutral errors into HTTP answers: 400 for validation, 403 for a disabled feature or a denied action, 429 for a rate limit, 409 or 422 for an idempotency conflict.
The root module
Section titled “The root module”/* Copyright (C) 2026-present Aristotelis — see repository license. */
import { contextSources } from '@common/context/context-sources.js';import { AuthSessionGuard } from '@common/guards/auth-session.guard.js';import { SessionPrincipalContextInterceptor } from '@common/interceptors/session-principal-context.interceptor.js';import { type MiddlewareConsumer, Module, type NestModule, StandardSchemaValidationPipe,} from '@nestjs/common';import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';import { CqrsModule } from '@nestjs/cqrs';import { CaslModule, UnauthorizedActionFilter } from '@nestjs-pipeline/casl';import { HttpCorrelationMiddleware } from '@nestjs-pipeline/correlation';import { FeatureDisabledFilter } from '@nestjs-pipeline/feature-flags';import { IdempotencyConflictFilter } from '@nestjs-pipeline/idempotency';import { JobContextModule } from '@nestjs-pipeline/job-context';import { RateLimitExceededFilter } from '@nestjs-pipeline/rate-limit';import { ZodValidationFilter, zodBadRequest } from '@nestjs-pipeline/zod';import { TenantSchemaMiddleware } from '@persistence/middlewares/tenant-schema.middleware.js';import { persistenceConfig } from '@persistence/persistence.config.js';import { PersistenceModule } from '@persistence/persistence.module.js';import { AuthorizationModule } from './auths/authorization.module.js';import { AuthsModule } from './auths/auths.module.js';import { SessionJobPrincipal } from './auths/infrastructure/session-job-principal.js';import { CaslPermissionSource } from './auths/persistence/casl-permission.source.js';import { DomainExceptionFilter } from './common/filters/domain-exception.filter.js';import { ObservabilityModule, ReliabilityModule,} from './common/modules/index.js';import { RolesModule } from './roles/roles.module.js';import { UsersModule } from './users/users.module.js';
/** * Root composition module of the Users API application. * * Orchestrates cross-cutting infrastructure concerns (Observability, Reliability, Persistence, * CASL Authorization, CQRS) alongside business domain modules (Users, Roles, Auths). * * ### Architectural Layout * - {@link ObservabilityModule}: Structured logging (Pino), OpenTelemetry tracing & metrics, global pipeline behaviors, and audit logging. * - {@link ReliabilityModule}: BullMQ queue engine, dead-letter storage, rate limiting, distributed idempotency, resilience policies, caching, and feature flags. * - {@link CaslModule}: Role- and attribute-based access control; {@link AuthorizationModule} supplies the request permission source. * - {@link PersistenceModule}: MikroORM database connection, entity repositories, and tenant schema manager. * - {@link JobContextModule}: carries a request's tenant, correlation id and principal into the jobs it enqueues. * - `StandardSchemaValidationPipe`: validates every route parameter declared with a `schema` and answers 400 with {@link zodBadRequest}'s body. * - Exception filters: map the packages' and the domain's framework-neutral errors to HTTP answers. * - Domain Feature Modules: {@link UsersModule}, {@link RolesModule}, {@link AuthsModule}. */@Module({ imports: [ CqrsModule.forRoot(), ObservabilityModule, ReliabilityModule, CaslModule.forRoot({ imports: [AuthorizationModule], permissionSource: { useExisting: CaslPermissionSource }, }), PersistenceModule, UsersModule, RolesModule, AuthsModule, JobContextModule.forRoot({ principal: SessionJobPrincipal, tenants: () => persistenceConfig().tenants, sources: contextSources, imports: [AuthsModule], }), ], providers: [ { provide: APP_GUARD, useClass: AuthSessionGuard }, { provide: APP_INTERCEPTOR, useClass: SessionPrincipalContextInterceptor }, { provide: APP_PIPE, useValue: new StandardSchemaValidationPipe({ exceptionFactory: zodBadRequest, }), }, { provide: APP_FILTER, useClass: ZodValidationFilter }, { provide: APP_FILTER, useClass: FeatureDisabledFilter }, { provide: APP_FILTER, useClass: RateLimitExceededFilter }, { provide: APP_FILTER, useClass: IdempotencyConflictFilter }, { provide: APP_FILTER, useClass: UnauthorizedActionFilter }, { provide: APP_FILTER, useClass: DomainExceptionFilter }, ],})export class AppModule implements NestModule { constructor( private readonly tenantSchemaMiddleware: TenantSchemaMiddleware, ) {}
configure(consumer: MiddlewareConsumer) { consumer .apply( HttpCorrelationMiddleware, this.tenantSchemaMiddleware.use.bind(this.tenantSchemaMiddleware), ) .forRoutes('*'); }}Global behaviors and observability
Section titled “Global behaviors and observability”ObservabilityModule orders the global behaviors so that telemetry wraps everything a
handler adds: logging, then the span and metrics, then AttributesBehavior, which copies
the add-ons’ decisions onto the span, then validation. Commands and events also get
DeadLetterBehavior. It also connects the tenant of @nestjs-pipeline/tenant to
@cqrs-ddd/core’s tenant-scoped cache keys:
/* Copyright (C) 2026-present Aristotelis — see repository license. */
import { IncomingMessage } from 'node:http';import { AUDIT_MODULE_DEFAULTS } from '@common/audit/audit.options.js';import { HEADERS } from '@common/constants/headers.constants.js';import { contextSources } from '@common/context/context-sources.js';import { HttpRouteInterceptor } from '@common/interceptors/http-route.interceptor.js';import { setTenantResolver } from '@cqrs-ddd/core/application';import { Module } from '@nestjs/common';import { APP_INTERCEPTOR } from '@nestjs/core';import { AuditModule } from '@nestjs-pipeline/audit';import { buildCacheAttributes } from '@nestjs-pipeline/cache';import { LOGGING_BEHAVIOR_LOGGER, logging, PipelineModule,} from '@nestjs-pipeline/core';import { buildDeadLetterAttributes, DeadLetterBehavior,} from '@nestjs-pipeline/deadletter';import { buildFeatureFlagAttributes } from '@nestjs-pipeline/feature-flags';import { buildIdempotencyAttributes } from '@nestjs-pipeline/idempotency';import { AttributesBehavior, MetricsBehavior, TraceBehavior,} from '@nestjs-pipeline/opentelemetry';import { buildRateLimitAttributes } from '@nestjs-pipeline/rate-limit';import { currentTenantId } from '@nestjs-pipeline/tenant';import { ZodValidationBehavior } from '@nestjs-pipeline/zod';import { LoggerModule, NativeLogger } from 'nestjs-pino';
/** Credential headers redacted from structured HTTP logs. */const HTTP_LOG_REDACT_PATHS = [ 'req.headers.authorization', 'req.headers.cookie', `req.headers["${HEADERS.API_KEY}"]`, `req.headers["${HEADERS.API_ID}"]`, 'req.headers["set-cookie"]', 'res.headers["set-cookie"]',];
/** * Configures structured HTTP logging, correlation propagation, tracing, metrics, * request validation and operational audit recording. Global pipeline ordering * keeps telemetry around handler-local behaviors so their outcomes reach the span. * * HTTP server spans carry the matched route (`HttpRouteInterceptor`). * * HTTP credentials are redacted through `HTTP_LOG_REDACT_PATHS`. Auditing uses * the default console sink with `failOpen: true`; durable audit requirements need * a persistent sink and an explicit failure policy. * * It also makes the pipeline's tenant the tenant of `@cqrs-ddd/core`'s * tenant-scoped helpers (`cacheKey`, `cacheKeyTemplate`), before any * lifecycle hook can start work that reads it. * * @example Register application observability * ```ts * @Module({ imports: [ObservabilityModule] }) * export class AppModule {} * ``` */@Module({ imports: [ LoggerModule.forRoot({ pinoHttp: { autoLogging: true, level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', redact: { paths: HTTP_LOG_REDACT_PATHS, censor: '[REDACTED]', }, transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true, messageFormat: '[{context}] {msg}', translateTime: 'SYS:HH:MM:ss.l', }, } : undefined, customProps: (req: IncomingMessage) => ({ context: `${req.method} ${req.url}`, }), }, }), PipelineModule.forRoot({ sources: contextSources, loggerProvider: { provide: LOGGING_BEHAVIOR_LOGGER, useExisting: NativeLogger, }, globalBehaviors: [ { scope: 'all', before: [ logging({ requestResponseLogLevel: 'log' }), [TraceBehavior, { tracerName: 'users-api' }], [MetricsBehavior, { meterName: 'users-api' }], [ AttributesBehavior, { factories: [ buildFeatureFlagAttributes, buildCacheAttributes, buildIdempotencyAttributes, buildRateLimitAttributes, buildDeadLetterAttributes, ], }, ], ZodValidationBehavior, ], }, { scope: 'commands', before: [DeadLetterBehavior], }, { scope: 'events', before: [DeadLetterBehavior], }, ], }), AuditModule.forRoot({ defaults: AUDIT_MODULE_DEFAULTS, }), ], providers: [{ provide: APP_INTERCEPTOR, useClass: HttpRouteInterceptor }], exports: [LoggerModule, PipelineModule, AuditModule],})export class ObservabilityModule { constructor() { setTenantResolver(currentTenantId); }}Queues, limits and stores
Section titled “Queues, limits and stores”ReliabilityModule connects the add-ons to their infrastructure: BullMQ queues, the
dead-letter transport, the rate limiter, the idempotency store, resilience policies, the
response cache and feature flags:
/* Copyright (C) 2026-present Aristotelis — see repository license. */
import { RATE_LIMIT_CAPACITY } from '@common/constants/index.js';import { redisConfig } from '@common/environment/redis.config.js';import { BullModule, getQueueToken } from '@nestjs/bullmq';import { Module } from '@nestjs/common';import { CacheModule } from '@nestjs-pipeline/cache';import { BullMqDeadLetterTransport, DeadLetterModule,} from '@nestjs-pipeline/deadletter';import { FeatureFlagsModule } from '@nestjs-pipeline/feature-flags';import { IdempotencyModule } from '@nestjs-pipeline/idempotency';import { RateLimitModule } from '@nestjs-pipeline/rate-limit';import { ResilienceModule } from '@nestjs-pipeline/resilience';import { TypedInMemoryProvider } from '@openfeature/server-sdk';import type { Queue } from 'bullmq';import { RateLimiterMemory } from 'rate-limiter-flexible';import { DEAD_LETTER_DEFAULTS } from '../dead-letter/dead-letter.options.js';
/** * Wires BullMQ dead-letter delivery, rate limiting, idempotency, resilience, * response caching and feature flags for handler-local pipeline configuration. * * Rate-limit quotas and idempotency records are process-local. Response caching * uses memory for local development and Redis when REDIS_HOST is configured or * NODE_ENV is production. Every Redis client takes its connection from * `redisConfig()`. Repository snapshot caching has its own adapters and * invalidation lifecycle; this module configures pipeline response caching. * * @example Register the pipeline infrastructure alongside observability * ```ts * @Module({ imports: [ObservabilityModule, ReliabilityModule] }) * export class AppModule {} * ``` */@Module({ imports: [ BullModule.forRootAsync({ useFactory: () => { const { host, port } = redisConfig(); return { connection: { host, port } }; }, }), DeadLetterModule.forRootAsync({ imports: [ BullModule.registerQueue({ name: 'dead-letters', forceDisconnectOnShutdown: true, }), ], inject: [getQueueToken('dead-letters')], useFactory: (queue: Queue) => new BullMqDeadLetterTransport(queue), defaults: DEAD_LETTER_DEFAULTS, }), RateLimitModule.forRoot({ limiter: new RateLimiterMemory(RATE_LIMIT_CAPACITY), }), IdempotencyModule.forRoot(), ResilienceModule.forRoot(), CacheModule.forRootAsync({ useFactory: () => { const redis = redisConfig(); return { store: !redis.isConfigured && process.env.NODE_ENV !== 'production' ? { type: 'memory' } : { type: 'redis', url: redis.url }, ttl: 30_000, }; }, }), FeatureFlagsModule.forRoot({ provider: new TypedInMemoryProvider({ 'user-registration': { disabled: false, variants: { on: true, off: false }, defaultVariant: 'on', }, 'role-creation': { disabled: false, variants: { on: true, off: false }, defaultVariant: 'on', }, }), context: { environment: process.env.NODE_ENV ?? 'development' }, }), ], exports: [ BullModule, DeadLetterModule, RateLimitModule, IdempotencyModule, ResilienceModule, CacheModule, FeatureFlagsModule, ],})export class ReliabilityModule {}