ation, other modules cannot accidentally bypass the repository pattern or access raw entity managers.
Step 2: Standardize the Validation Pipeline
Request validation must occur at the controller boundary using DTOs and the global ValidationPipe. AI should never generate manual body parsing or inline validation logic.
// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
await app.listen(3000);
}
bootstrap();
// inventory/dto/create-inventory-item.dto.ts
import { IsString, IsNumber, Min, IsOptional } from 'class-validator';
export class CreateInventoryItemDto {
@IsString()
readonly sku: string;
@IsNumber()
@Min(0)
readonly quantity: number;
@IsOptional()
@IsString()
readonly warehouseLocation?: string;
}
Rationale: The whitelist and forbidNonWhitelisted flags prevent mass assignment vulnerabilities. transform: true ensures incoming strings are cast to their declared types, eliminating manual type coercion in services.
Services should return domain objects or plain data structures. Response formatting, envelope wrapping, and metadata injection belong in interceptors.
// common/interceptors/standard-response.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface StandardApiResponse<T> {
payload: T;
requestId: string;
timestamp: string;
}
@Injectable()
export class StandardResponseInterceptor<T>
implements NestInterceptor<T, StandardApiResponse<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<StandardApiResponse<T>> {
const requestId = context.switchToHttp().getRequest()['x-request-id'];
return next.handle().pipe(
map((data) => ({
payload: data,
requestId: requestId || 'unknown',
timestamp: new Date().toISOString(),
})),
);
}
}
Rationale: Separating transformation from business logic keeps services focused on domain rules. Interceptors run after service execution but before serialization, guaranteeing consistent API contracts across all endpoints.
Step 4: Centralize Error Handling
Controllers must never contain try/catch blocks. Domain-specific exceptions should be thrown by services and caught by a global exception filter that formats the response.
// common/filters/domain-error.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class DomainErrorFilter implements ExceptionFilter {
private readonly logger = new Logger(DomainErrorFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof Error && 'status' in exception
? (exception as any).status
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof Error ? exception.message : 'Internal server error';
this.logger.error(
`${request.method} ${request.url} ${status}`,
exception instanceof Error ? exception.stack : undefined,
);
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}
Rationale: Global filters guarantee uniform error payloads for frontend consumers and monitoring tools. They also prevent controllers from becoming bloated with error formatting logic.
Step 5: Mandate Configuration & Lifecycle Management
Environment variables must never be accessed directly. A configuration service should validate and expose settings. Services holding external connections must implement lifecycle hooks to prevent handle leaks.
// config/environment-config.service.ts
import { Injectable } from '@nestjs/common';
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(['error', 'warn', 'log', 'debug']).default('log'),
});
@Injectable()
export class EnvironmentConfigService {
private readonly validatedEnv: z.infer<typeof envSchema>;
constructor() {
this.validatedEnv = envSchema.parse(process.env);
}
get<T extends keyof z.infer<typeof envSchema>>(key: T): z.infer<typeof envSchema>[T] {
return this.validatedEnv[key];
}
}
// messaging/queue-manager.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { Queue } from 'bullmq';
@Injectable()
export class QueueManagerService implements OnModuleInit, OnModuleDestroy {
private readonly eventQueue: Queue;
constructor(private readonly config: EnvironmentConfigService) {
this.eventQueue = new Queue('background-tasks', {
connection: { url: this.config.get('REDIS_URL') },
});
}
async onModuleInit(): Promise<void> {
await this.eventQueue.waitUntilReady();
}
async onModuleDestroy(): Promise<void> {
await this.eventQueue.close();
}
}
Rationale: Zod validation at startup fails fast if required variables are missing or malformed. Lifecycle hooks ensure Redis, database, and queue connections close gracefully during shutdown or test teardown, eliminating --detectOpenHandles warnings.
Pitfall Guide
1. Direct Cross-Module Service Injection
Explanation: AI models frequently import services directly from other modules (e.g., import { UserService } from '../user/user.service'). This breaks module encapsulation and creates tight coupling.
Fix: Enforce the SharedModule pattern. Export only interfaces, DTOs, or infrastructure providers. Require feature modules to import dependencies explicitly through module imports, not file paths.
2. Controller-Level Error Handling
Explanation: Generated controllers often wrap service calls in try/catch blocks and manually format error responses. This duplicates logic and bypasses global error handling.
Fix: Throw domain-specific exceptions (e.g., NotFoundException, ConflictException) from services. Register a global exception filter in main.ts. Controllers should only map HTTP methods to service calls.
3. Mixed Persistence Strategies
Explanation: AI may generate some repositories using TypeORM's EntityManager while others use Prisma clients or raw SQL queries. This creates inconsistent transaction handling and testing strategies.
Fix: Declare a single ORM in the AI context file. Enforce the repository pattern for all data access. Restrict raw queries to explicitly typed, named methods within repository classes.
4. Synchronous Side Effects in Request Handlers
Explanation: AI often places email dispatch, webhook calls, or analytics tracking directly in controller methods. This blocks the event loop and degrades throughput under concurrent load.
Fix: Route all side effects through a message broker (BullMQ, RabbitMQ, or AWS SQS). Controllers should enqueue tasks and return immediately. Processors run in dedicated worker processes.
5. Unmanaged Connection Lifecycles
Explanation: Services that open database, Redis, or HTTP client connections without cleanup cause test flakiness and prevent graceful application shutdown.
Fix: Implement OnModuleInit for connection readiness checks and OnModuleDestroy for explicit closure. Verify with Jest's --detectOpenHandles flag during CI.
6. Bypassing the Validation Pipe
Explanation: AI may generate manual validation logic inside services or use any types for request bodies, assuming validation is handled elsewhere.
Fix: Mandate class-validator decorators on all DTOs. Configure the global ValidationPipe with whitelist: true and forbidNonWhitelisted: true. Reject any controller method that accesses req.body directly.
7. Hardcoded Environment Access
Explanation: Direct process.env.DATABASE_URL calls scattered across services break test isolation and prevent configuration validation.
Fix: Centralize environment access through a typed configuration service. Validate schema at bootstrap. Inject the service via DI. Never reference process.env outside the configuration module.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Rapid Prototyping / MVP | Prisma + BullMQ + Zod Config | Faster schema migrations, simpler query builder, strict runtime validation | Low initial setup, moderate scaling cost |
| Enterprise Microservice | TypeORM + EventEmitter2 + Joi Config | Mature decorator ecosystem, explicit transaction control, team familiarity | Higher initial boilerplate, lower long-term maintenance |
| Data-Intensive Platform | TypeORM + Raw Query Repositories + Redis Streams | Optimized for complex joins, batch processing, and high-throughput event streaming | Higher infrastructure cost, requires dedicated DBA oversight |
| Strict Compliance / Audit | Global Exception Filters + Swagger + Structured Logger | Enforces consistent error contracts, auto-generates API docs, enables log aggregation | Moderate dev overhead, significant compliance savings |
Configuration Template
# AI Architecture Context: NestJS
## Stack Definition
- Framework: NestJS (latest stable)
- ORM: [TypeORM | Prisma] (select one, never mix)
- Queue: BullMQ + Redis
- Validation: class-validator + class-transformer
- Auth: Passport + JWT Strategy
- Config: Zod runtime validation via ConfigService
## Architectural Constraints
- Module boundaries are strict: no cross-module service imports. Use SharedModule for shared infrastructure.
- All HTTP request bodies must use DTOs with class-validator decorators. No direct req.body access.
- ValidationPipe is global: whitelist: true, forbidNonWhitelisted: true, transform: true.
- Database access uses the repository pattern. No EntityManager/Prisma client calls in services.
- Response transformation (envelopes, metadata) belongs in interceptors. Services return domain objects.
- Authentication/authorization uses Guards. No middleware or manual JWT decoding in controllers.
- GlobalExceptionFilter handles all errors. No try/catch blocks in controllers.
- Environment variables accessed only through ConfigService. No process.env outside config modules.
- Side effects (emails, webhooks, analytics) route through BullMQ queues. Never synchronous in request handlers.
- Swagger decorators (@ApiTags, @ApiProperty, @ApiResponse) are mandatory on all controllers and DTOs.
- Unit tests mock all providers via Test.createTestingModule(). E2E tests use full application modules.
- Logger class injected in every service with explicit context. No console.log anywhere.
- Services holding external connections implement OnModuleInit and OnModuleDestroy.
## File Structure Convention
feature/
feature.module.ts
feature.controller.ts
feature.service.ts
dto/
create-feature.dto.ts
update-feature.dto.ts
entities/
feature.entity.ts
feature.service.spec.ts
feature.e2e-spec.ts
## Prohibited Patterns
- Circular module dependencies (use EventEmitter2 or domain events instead)
- Raw process.env references outside configuration modules
- console.log, console.error, or debug prints in production code
- try/catch blocks in controller methods
- Direct ORM client calls in service classes
- Synchronous I/O in HTTP request handlers
Quick Start Guide
- Create the context file: Save the Configuration Template above as
AI_ARCHITECTURE.md in your project root.
- Configure your AI assistant: Point your AI coding tool to the context file. Ensure it reads the file before generating any NestJS code.
- Initialize the scaffolding: Run
nest g module shared-infrastructure and nest g module core-config. Implement the global pipe, filter, and interceptor in main.ts as shown in the Core Solution.
- Validate with linting: Add
eslint-plugin-nestjs and @typescript-eslint/no-floating-promises to your CI pipeline. AI-generated code that violates module boundaries or lifecycle rules will fail checks before review.
- Test the constraint loop: Generate a new feature using your AI assistant. Verify that DTOs include validation decorators, controllers lack try/catch blocks, and services use injected repositories. If deviations occur, refine the context file with explicit negative constraints.