Skip to content

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.

  1. HttpCorrelationMiddleware sets the correlation id; TenantSchemaMiddleware runs the rest of the request inside the tenant named by x-tenant-schema (see Several tenants in one deployment).
  2. AuthSessionGuard resolves the principal; SessionPrincipalContextInterceptor makes it available to the application; HttpRouteInterceptor names the HTTP span by its route.
  3. StandardSchemaValidationPipe validates the parameters declared with a schema.
  4. The controller dispatches a command or query; the bus runs its pipeline: the global before behaviors, the handler’s own, then the handler.
  5. 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.
api/src/app.module.ts
/* 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('*');
}
}

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:

api/src/common/modules/observability.module.ts
/* 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);
}
}

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:

api/src/common/modules/reliability.module.ts
/* 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 {}