Production Operations in NestJS: Comprehensive Testing, Observability, and Security Hardening
Code that functions is common; code that lasts is rare. This guide bridges the gap between development and operations (DevSecOps). We explore advanced testing patterns, build a complete observability stack using OpenTelemetry and Prometheus, and lock down your application using industry-standard security practices for encryption, sanitization, and headers.
Testing
Unit Testing with Jest
NestJS uses Jest as its default testing framework, providing a @nestjs/testing package that creates isolated testing modules where you can instantiate components with mocked dependencies, enabling fast, focused tests that verify individual units of code in isolation.
// user.service.spec.ts describe('UserService', () => { let service: UserService; let mockRepository: jest.Mocked<Repository<User>>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: getRepositoryToken(User), useValue: { find: jest.fn(), findOne: jest.fn(), save: jest.fn(), delete: jest.fn(), }, }, ], }).compile(); service = module.get<UserService>(UserService); mockRepository = module.get(getRepositoryToken(User)); }); describe('findById', () => { it('should return a user when found', async () => { const expectedUser = { id: '1', name: 'John', email: 'john@test.com' }; mockRepository.findOne.mockResolvedValue(expectedUser); const result = await service.findById('1'); expect(result).toEqual(expectedUser); expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: '1' } }); }); it('should throw NotFoundException when user not found', async () => { mockRepository.findOne.mockResolvedValue(null); await expect(service.findById('999')).rejects.toThrow(NotFoundException); }); }); });
Mocking Providers
Mocking providers involves replacing real dependencies with fake implementations using Jest mocks or custom test doubles, allowing you to control behavior, avoid external calls (databases, APIs), and verify interactions without side effects.
// Different mocking strategies describe('OrderService', () => { let service: OrderService; // Strategy 1: Simple value mock const mockPaymentService = { processPayment: jest.fn().mockResolvedValue({ success: true }), refund: jest.fn(), }; // Strategy 2: Factory function for fresh mocks const createMockInventoryService = () => ({ checkStock: jest.fn(), reserve: jest.fn(), release: jest.fn(), }); // Strategy 3: Class-based mock class MockEmailService { send = jest.fn().mockResolvedValue(true); sendBulk = jest.fn(); } beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ OrderService, { provide: PaymentService, useValue: mockPaymentService }, { provide: InventoryService, useFactory: createMockInventoryService }, { provide: EmailService, useClass: MockEmailService }, { provide: ConfigService, useValue: { get: () => 'test-value' } }, ], }).compile(); service = module.get<OrderService>(OrderService); }); it('should process order with payment', async () => { const order = await service.create({ items: [], total: 100 }); expect(mockPaymentService.processPayment).toHaveBeenCalledWith(100); expect(order.status).toBe('confirmed'); }); });
Testing Controllers
Controller tests verify HTTP layer behavior including route handling, request validation, response formatting, and proper delegation to services, using the testing module to instantiate controllers with mocked service dependencies.
describe('UserController', () => { let controller: UserController; let mockUserService: jest.Mocked<UserService>; beforeEach(async () => { const module = await Test.createTestingModule({ controllers: [UserController], providers: [ { provide: UserService, useValue: { findAll: jest.fn(), findById: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), }, }, ], }).compile(); controller = module.get<UserController>(UserController); mockUserService = module.get(UserService); }); describe('GET /users', () => { it('should return array of users', async () => { const users = [{ id: '1', name: 'John' }]; mockUserService.findAll.mockResolvedValue(users); const result = await controller.findAll(); expect(result).toEqual(users); }); }); describe('POST /users', () => { it('should create and return new user', async () => { const createDto = { name: 'Jane', email: 'jane@test.com' }; const createdUser = { id: '2', ...createDto }; mockUserService.create.mockResolvedValue(createdUser); const result = await controller.create(createDto); expect(mockUserService.create).toHaveBeenCalledWith(createDto); expect(result).toEqual(createdUser); }); }); });
Testing Services
Service tests focus on business logic verification, testing that services correctly orchestrate operations, handle edge cases, throw appropriate exceptions, and interact properly with their dependencies like repositories and external APIs.
describe('PaymentService', () => { let service: PaymentService; let mockStripeClient: jest.Mocked<StripeClient>; let mockPaymentRepo: jest.Mocked<Repository<Payment>>; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ PaymentService, { provide: StripeClient, useValue: { createCharge: jest.fn(), refund: jest.fn(), }, }, { provide: getRepositoryToken(Payment), useValue: { save: jest.fn(), findOne: jest.fn(), }, }, ], }).compile(); service = module.get(PaymentService); mockStripeClient = module.get(StripeClient); mockPaymentRepo = module.get(getRepositoryToken(Payment)); }); describe('processPayment', () => { it('should create charge and save payment record', async () => { mockStripeClient.createCharge.mockResolvedValue({ id: 'ch_123' }); mockPaymentRepo.save.mockResolvedValue({ id: 'pay_1', stripeId: 'ch_123' }); const result = await service.processPayment({ amount: 1000, currency: 'usd', customerId: 'cust_1', }); expect(mockStripeClient.createCharge).toHaveBeenCalledWith(1000, 'usd'); expect(mockPaymentRepo.save).toHaveBeenCalled(); expect(result.stripeId).toBe('ch_123'); }); it('should throw on payment failure', async () => { mockStripeClient.createCharge.mockRejectedValue(new Error('Card declined')); await expect( service.processPayment({ amount: 1000, currency: 'usd', customerId: 'cust_1' }) ).rejects.toThrow('Payment processing failed'); }); }); });
E2E Testing
End-to-end tests verify the complete request/response cycle by making actual HTTP requests to your running application, testing the full integration of controllers, services, guards, pipes, and database interactions in a production-like environment.
// test/app.e2e-spec.ts describe('Users API (e2e)', () => { let app: INestApplication; let authToken: string; beforeAll(async () => { const moduleFixture = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(EmailService) .useValue({ send: jest.fn() }) // Mock external services .compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe()); await app.init(); // Get auth token for protected routes const loginResponse = await request(app.getHttpServer()) .post('/auth/login') .send({ email: 'admin@test.com', password: 'password' }); authToken = loginResponse.body.accessToken; }); afterAll(async () => { await app.close(); }); describe('POST /users', () => { it('should create a new user', () => { return request(app.getHttpServer()) .post('/users') .set('Authorization', `Bearer ${authToken}`) .send({ name: 'Test User', email: 'test@example.com', password: 'Pass123!' }) .expect(201) .expect((res) => { expect(res.body).toHaveProperty('id'); expect(res.body.email).toBe('test@example.com'); }); }); it('should return 400 for invalid email', () => { return request(app.getHttpServer()) .post('/users') .set('Authorization', `Bearer ${authToken}`) .send({ name: 'Test', email: 'invalid-email', password: 'Pass123!' }) .expect(400); }); }); });
Testing Guards and Interceptors
Guards and interceptors require testing their decision logic and transformation behavior by creating minimal execution contexts, verifying they correctly allow/deny access or properly transform requests and responses based on various conditions.
// Testing a Guard describe('RolesGuard', () => { let guard: RolesGuard; let reflector: Reflector; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [RolesGuard, Reflector], }).compile(); guard = module.get<RolesGuard>(RolesGuard); reflector = module.get<Reflector>(Reflector); }); const createMockContext = (user: any) => ({ switchToHttp: () => ({ getRequest: () => ({ user }), }), getHandler: () => jest.fn(), getClass: () => jest.fn(), }) as unknown as ExecutionContext; it('should allow admin access to admin routes', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']); const context = createMockContext({ roles: ['admin'] }); expect(guard.canActivate(context)).toBe(true); }); it('should deny user access to admin routes', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']); const context = createMockContext({ roles: ['user'] }); expect(guard.canActivate(context)).toBe(false); }); }); // Testing an Interceptor describe('LoggingInterceptor', () => { let interceptor: LoggingInterceptor; it('should log request duration', async () => { interceptor = new LoggingInterceptor(); const logSpy = jest.spyOn(console, 'log'); const context = { switchToHttp: () => ({ getRequest: () => ({ url: '/test' }) }) }; const next = { handle: () => of({ data: 'test' }) }; await interceptor.intercept(context as any, next as any).toPromise(); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Request to /test')); }); });
Testing Middleware
Middleware tests verify request/response manipulation and proper next() calling by creating mock request/response objects and checking that headers, body modifications, or early terminations happen as expected.
describe('LoggerMiddleware', () => { let middleware: LoggerMiddleware; let mockRequest: Partial<Request>; let mockResponse: Partial<Response>; let nextFunction: jest.Mock; beforeEach(() => { middleware = new LoggerMiddleware(); mockRequest = { method: 'GET', originalUrl: '/api/users', headers: { 'user-agent': 'jest-test' }, ip: '127.0.0.1', }; mockResponse = { on: jest.fn(), statusCode: 200, }; nextFunction = jest.fn(); }); it('should call next()', () => { middleware.use(mockRequest as Request, mockResponse as Response, nextFunction); expect(nextFunction).toHaveBeenCalled(); }); it('should log request details', () => { const logSpy = jest.spyOn(console, 'log'); middleware.use(mockRequest as Request, mockResponse as Response, nextFunction); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('GET /api/users')); }); }); // Testing functional middleware describe('corsMiddleware', () => { it('should set CORS headers', () => { const mockRes = { setHeader: jest.fn(), }; const mockNext = jest.fn(); corsMiddleware({} as Request, mockRes as any, mockNext); expect(mockRes.setHeader).toHaveBeenCalledWith( 'Access-Control-Allow-Origin', '*' ); expect(mockNext).toHaveBeenCalled(); }); });
Database Testing Strategies
Database testing strategies range from using in-memory databases (SQLite) for speed, to test containers for production parity, to seeding test data with factories, while ensuring proper isolation through transactions that rollback after each test.
// Strategy 1: In-memory SQLite for speed const testModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [User, Order], synchronize: true, }), ], }).compile(); // Strategy 2: Test containers for production parity import { PostgreSqlContainer } from '@testcontainers/postgresql'; describe('UserRepository (Integration)', () => { let container: StartedPostgreSqlContainer; let dataSource: DataSource; beforeAll(async () => { container = await new PostgreSqlContainer().start(); dataSource = new DataSource({ type: 'postgres', host: container.getHost(), port: container.getPort(), database: container.getDatabase(), username: container.getUsername(), password: container.getPassword(), entities: [User], synchronize: true, }); await dataSource.initialize(); }, 60000); afterAll(async () => { await dataSource.destroy(); await container.stop(); }); }); // Strategy 3: Transaction rollback for isolation beforeEach(async () => { queryRunner = dataSource.createQueryRunner(); await queryRunner.startTransaction(); }); afterEach(async () => { await queryRunner.rollbackTransaction(); await queryRunner.release(); }); // Strategy 4: Factories for test data // user.factory.ts export const createTestUser = (overrides?: Partial<User>): User => ({ id: faker.string.uuid(), email: faker.internet.email(), name: faker.person.fullName(), ...overrides, });
Test Coverage
Test coverage measures what percentage of your code is executed during tests, with Jest providing built-in coverage reports showing line, branch, function, and statement coverage—aim for meaningful coverage of critical paths rather than arbitrary percentage targets.
// package.json scripts { "scripts": { "test": "jest", "test:cov": "jest --coverage", "test:cov:ci": "jest --coverage --coverageReporters=lcov --coverageReporters=text" } } // jest.config.js module.exports = { collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.spec.ts', '!src/**/*.module.ts', '!src/main.ts', ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, './src/services/': { branches: 90, lines: 90, }, }, coverageDirectory: './coverage', coverageReporters: ['text', 'lcov', 'html'], }; // Coverage report output: // ┌─────────────────────┬─────────┬─────────┬─────────┬─────────┐ // │ File │ % Stmts │ % Branch│ % Funcs │ % Lines │ // ├─────────────────────┼─────────┼─────────┼─────────┼─────────┤ // │ All files │ 85.42 │ 78.26 │ 89.47 │ 85.00 │ // │ user.service.ts │ 92.31 │ 85.71 │ 100.00 │ 91.67 │ // │ order.service.ts │ 78.57 │ 66.67 │ 80.00 │ 78.57 │ // └─────────────────────┴─────────┴─────────┴─────────┴─────────┘
Integration Testing
Integration tests verify that multiple components work correctly together (services + repositories + database), focusing on the boundaries between units and catching issues that unit tests miss, while still being faster than full E2E tests.
describe('OrderService Integration', () => { let orderService: OrderService; let userService: UserService; let dataSource: DataSource; beforeAll(async () => { const module = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [User, Order, OrderItem, Product], synchronize: true, }), TypeOrmModule.forFeature([User, Order, OrderItem, Product]), ], providers: [OrderService, UserService, ProductService], }).compile(); orderService = module.get(OrderService); userService = module.get(UserService); dataSource = module.get(DataSource); }); beforeEach(async () => { // Seed test data await dataSource.getRepository(User).save({ id: '1', name: 'Test User' }); await dataSource.getRepository(Product).save([ { id: 'p1', name: 'Widget', price: 10, stock: 100 }, { id: 'p2', name: 'Gadget', price: 20, stock: 50 }, ]); }); afterEach(async () => { await dataSource.synchronize(true); // Reset database }); it('should create order and update inventory', async () => { const order = await orderService.create({ userId: '1', items: [ { productId: 'p1', quantity: 2 }, { productId: 'p2', quantity: 1 }, ], }); expect(order.total).toBe(40); // (10*2) + (20*1) const product = await dataSource.getRepository(Product).findOneBy({ id: 'p1' }); expect(product.stock).toBe(98); // 100 - 2 }); });
Contract Testing
Contract testing verifies that service APIs conform to agreed-upon contracts between consumers and providers, typically using tools like Pact to generate and verify contracts, ensuring microservices can evolve independently without breaking integrations.
// Consumer side (API Gateway) import { PactV3, MatchersV3 } from '@pact-foundation/pact'; const { like, eachLike } = MatchersV3; describe('User Service Contract', () => { const provider = new PactV3({ consumer: 'APIGateway', provider: 'UserService', }); it('should return user by id', async () => { await provider .given('user with id 1 exists') .uponReceiving('a request for user 1') .withRequest({ method: 'GET', path: '/users/1', }) .willRespondWith({ status: 200, body: like({ id: '1', name: 'John Doe', email: 'john@example.com', }), }) .executeTest(async (mockServer) => { const client = new UserClient(mockServer.url); const user = await client.getUser('1'); expect(user.id).toBe('1'); }); }); }); // Provider side verification describe('Pact Verification', () => { it('validates the expectations of APIGateway', async () => { const verifier = new Verifier({ providerBaseUrl: 'http://localhost:3001', pactUrls: ['./pacts/apigateway-userservice.json'], stateHandlers: { 'user with id 1 exists': async () => { await seedUser({ id: '1', name: 'John Doe' }); }, }, }); await verifier.verifyProvider(); }); }); // Contract testing flow: // ┌────────────┐ ┌──────────────┐ ┌────────────┐ // │ Consumer │────►│ Contract │◄────│ Provider │ // │ Tests │ │ (Pact) │ │ Tests │ // └────────────┘ └──────────────┘ └────────────┘ // │ │ │ // │ Generate │ Verify │ // └───────────────────┴────────────────────┘
Logging & Monitoring
Built-in Logger
NestJS provides a built-in Logger class that supports multiple log levels and context-based logging out of the box, automatically formatting output with timestamps and colors in development.
import { Logger } from '@nestjs/common'; @Injectable() export class UserService { private readonly logger = new Logger(UserService.name); createUser(data: CreateUserDto) { this.logger.log('Creating user...'); this.logger.debug('User data: ' + JSON.stringify(data)); this.logger.warn('Deprecated method used'); this.logger.error('Failed to create user', error.stack); } }
Custom Logger Implementation
You can replace the default logger by implementing the LoggerService interface, giving you full control over log formatting, destinations, and behavior across your entire application.
import { LoggerService, Injectable } from '@nestjs/common'; @Injectable() export class CustomLogger implements LoggerService { log(message: string, context?: string) { console.log(`[${new Date().toISOString()}] [${context}] ${message}`); } error(message: string, trace?: string, context?: string) { console.error(`[ERROR] [${context}] ${message}\n${trace}`); } warn(message: string, context?: string) { console.warn(`[WARN] [${context}] ${message}`); } debug(message: string, context?: string) { console.debug(`[DEBUG] [${context}] ${message}`); } } // main.ts const app = await NestFactory.create(AppModule, { bufferLogs: true }); app.useLogger(new CustomLogger());
Winston Integration
Winston is a versatile logging library that supports multiple transports (console, file, HTTP), log rotation, and structured logging - integrate it by wrapping it in a NestJS provider.
import { WinstonModule } from 'nest-winston'; import * as winston from 'winston'; @Module({ imports: [ WinstonModule.forRoot({ transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.timestamp(), winston.format.colorize(), winston.format.printf(({ timestamp, level, message, context }) => `${timestamp} [${context}] ${level}: ${message}` ), ), }), new winston.transports.File({ filename: 'app.log' }), ], }), ], }) export class AppModule {}
Pino Integration
Pino is an extremely fast, low-overhead JSON logger ideal for production environments; nestjs-pino provides seamless integration with automatic request logging and context propagation.
import { LoggerModule } from 'nestjs-pino'; @Module({ imports: [ LoggerModule.forRoot({ pinoHttp: { transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true } } : undefined, level: process.env.LOG_LEVEL || 'info', redact: ['req.headers.authorization'], // Hide sensitive data }, }), ], }) export class AppModule {} // Output: {"level":30,"time":1234567890,"req":{"method":"GET"},"msg":"request completed"}
Log Levels and Formatting
Log levels control verbosity (error → warn → log → debug → verbose), and proper formatting ensures logs are parseable by log aggregation systems like ELK or Splunk.
┌─────────────────────────────────────────────────┐
│ LOG LEVELS HIERARCHY │
├─────────────────────────────────────────────────┤
│ VERBOSE ← Most detailed (development) │
│ DEBUG ← Debugging information │
│ LOG ← General information (default) │
│ WARN ← Warning messages │
│ ERROR ← Error messages only (production) │
└─────────────────────────────────────────────────┘
// Configure in main.ts
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'], // Only these levels
});
Request Logging
Request logging middleware captures incoming HTTP requests and outgoing responses, tracking method, URL, status code, response time, and client information for debugging and analytics.
import { Injectable, NestMiddleware, Logger } from '@nestjs/common'; @Injectable() export class RequestLoggerMiddleware implements NestMiddleware { private logger = new Logger('HTTP'); use(req: Request, res: Response, next: NextFunction) { const { method, originalUrl } = req; const start = Date.now(); res.on('finish', () => { const { statusCode } = res; const duration = Date.now() - start; this.logger.log(`${method} ${originalUrl} ${statusCode} - ${duration}ms`); }); next(); } } // Output: [HTTP] GET /api/users 200 - 45ms
Correlation IDs
Correlation IDs (trace IDs) are unique identifiers attached to each request that propagate through all service calls, enabling you to trace a single request across distributed systems and logs.
import { v4 as uuidv4 } from 'uuid'; import { AsyncLocalStorage } from 'async_hooks'; export const asyncLocalStorage = new AsyncLocalStorage<{ correlationId: string }>(); @Injectable() export class CorrelationMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const correlationId = req.headers['x-correlation-id'] as string || uuidv4(); res.setHeader('x-correlation-id', correlationId); asyncLocalStorage.run({ correlationId }, () => next()); } } // In logger: { correlationId: "abc-123", message: "User created" }
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │───▶│ Service A│───▶│ Service B│
│ │ │ ID:abc123│ │ ID:abc123│
└──────────┘ └──────────┘ └──────────┘
│ │ │
└───────────────┴───────────────┘
All logs share: abc123
Distributed Tracing
Distributed tracing tracks requests across microservices, recording spans (timing units) that show the complete journey of a request, helping identify bottlenecks and failures in complex architectures.
// Using Jaeger with OpenTracing import { initTracer } from 'jaeger-client'; const config = { serviceName: 'user-service', sampler: { type: 'const', param: 1 }, reporter: { agentHost: 'jaeger', agentPort: 6832 }, }; const tracer = initTracer(config, {}); @Injectable() export class TracingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler) { const span = tracer.startSpan('http_request'); return next.handle().pipe( finalize(() => span.finish()) ); } }
Request Timeline:
├─ API Gateway (50ms) ──────────────────────────┤
│ ├─ Auth Service (10ms) ────┤ │
│ ├─ User Service (25ms) ─────────────┤ │
│ │ └─ Database Query (15ms) ───┤ │ │
│ └─ Response │
OpenTelemetry Integration
OpenTelemetry is the vendor-neutral standard for observability, providing unified APIs for traces, metrics, and logs that can export to various backends (Jaeger, Zipkin, Datadog, etc.).
// tracing.ts - Initialize before app starts import { NodeSDK } from '@opentelemetry/sdk-node'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; const sdk = new NodeSDK({ serviceName: 'nestjs-app', traceExporter: new OTLPTraceExporter({ url: 'http://otel-collector:4318/v1/traces', }), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); // Auto-instruments: HTTP, Express, pg, redis, etc.
Prometheus Metrics
Prometheus is a time-series database for metrics; expose application metrics (request count, latency histograms, memory usage) via an endpoint that Prometheus scrapes periodically.
import { PrometheusModule, makeCounterProvider } from '@willsoto/nestjs-prometheus'; @Module({ imports: [ PrometheusModule.register({ path: '/metrics', defaultMetrics: { enabled: true } }), ], providers: [ makeCounterProvider({ name: 'http_requests_total', help: 'Total HTTP requests' }), ], }) export class AppModule {} @Controller() export class AppController { constructor(@InjectMetric('http_requests_total') private counter: Counter) {} @Get() getData() { this.counter.inc({ method: 'GET', path: '/' }); return 'data'; } } // GET /metrics returns: // http_requests_total{method="GET",path="/"} 42
Health Checks (Terminus)
Terminus module provides standardized health check endpoints for Kubernetes liveness/readiness probes, verifying database connections, disk space, memory, and external service availability.
import { TerminusModule } from '@nestjs/terminus'; @Controller('health') export class HealthController { constructor( private health: HealthCheckService, private db: TypeOrmHealthIndicator, private http: HttpHealthIndicator, private disk: DiskHealthIndicator, private memory: MemoryHealthIndicator, ) {} @Get() @HealthCheck() check() { return this.health.check([ () => this.db.pingCheck('database'), () => this.http.pingCheck('api', 'https://api.example.com'), () => this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }), () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), ]); } } // Response: { status: 'ok', details: { database: { status: 'up' }, ... } }
Security
CORS Configuration
Cross-Origin Resource Sharing (CORS) controls which domains can access your API; configure it to allow specific origins, methods, and headers while blocking unauthorized cross-origin requests.
// main.ts const app = await NestFactory.create(AppModule); // Simple: allow specific origin app.enableCors({ origin: ['https://myapp.com', 'https://admin.myapp.com'], methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 3600, }); // Dynamic: validate origin at runtime app.enableCors({ origin: (origin, callback) => { const allowedOrigins = configService.get('ALLOWED_ORIGINS').split(','); if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, });
CSRF Protection
Cross-Site Request Forgery protection prevents attackers from tricking authenticated users into executing unwanted actions; implement using tokens that must accompany state-changing requests.
import * as csurf from 'csurf'; import * as cookieParser from 'cookie-parser'; // main.ts app.use(cookieParser()); app.use(csurf({ cookie: { httpOnly: true, sameSite: 'strict' } })); @Controller() export class AppController { @Get('csrf-token') getCsrfToken(@Req() req) { return { csrfToken: req.csrfToken() }; } } // Client must send token in header: X-CSRF-Token
┌─────────────────────────────────────────────────────┐
│ 1. GET /csrf-token → { token: "abc123" } │
│ 2. POST /api/action + Header: X-CSRF-Token: abc123 │
│ 3. Server validates token matches session │
└─────────────────────────────────────────────────────┘
SQL Injection Prevention
SQL injection is prevented by using parameterized queries and ORMs like TypeORM/Prisma, which automatically escape user input and never concatenate raw user data into SQL strings.
// ❌ DANGEROUS: SQL Injection vulnerable const users = await dataSource.query( `SELECT * FROM users WHERE name = '${userInput}'` ); // ✅ SAFE: Parameterized query const users = await dataSource.query( 'SELECT * FROM users WHERE name = $1', [userInput] ); // ✅ SAFE: TypeORM Query Builder const users = await this.userRepository .createQueryBuilder('user') .where('user.name = :name', { name: userInput }) .getMany(); // ✅ SAFE: TypeORM Repository methods const user = await this.userRepository.findOne({ where: { email: userInput } // Automatically parameterized });
XSS Prevention
Cross-Site Scripting (XSS) prevention involves sanitizing user input, encoding output, and setting proper Content Security Policy headers to prevent malicious scripts from executing in browsers.
import * as helmet from 'helmet'; import * as sanitizeHtml from 'sanitize-html'; // main.ts - Security headers including CSP app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], }, }, })); // Sanitize user input @Injectable() export class SanitizePipe implements PipeTransform { transform(value: any) { if (typeof value === 'string') { return sanitizeHtml(value, { allowedTags: [], allowedAttributes: {} }); } return value; } } @Post() createComment(@Body('content', SanitizePipe) content: string) {}
Request Validation
Request validation using class-validator decorators and ValidationPipe ensures all incoming data meets expected formats, types, and constraints before reaching your business logic.
import { IsEmail, IsString, MinLength, IsInt, Min, Max, IsOptional } from 'class-validator'; import { Transform, Type } from 'class-transformer'; export class CreateUserDto { @IsEmail() @Transform(({ value }) => value.toLowerCase().trim()) email: string; @IsString() @MinLength(8) password: string; @IsInt() @Min(18) @Max(120) @Type(() => Number) age: number; @IsOptional() @IsString() bio?: string; } // main.ts - Global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip unknown properties forbidNonWhitelisted: true, // Throw on unknown properties transform: true, // Auto-transform types }));
Encryption and Hashing
Use bcrypt for password hashing (one-way) and crypto/AES for reversible encryption of sensitive data; never store passwords in plain text or use weak algorithms like MD5.
import * as bcrypt from 'bcrypt'; import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; @Injectable() export class CryptoService { // Password hashing (one-way) async hashPassword(password: string): Promise<string> { const salt = await bcrypt.genSalt(12); return bcrypt.hash(password, salt); } async verifyPassword(password: string, hash: string): Promise<boolean> { return bcrypt.compare(password, hash); } // Data encryption (reversible) encrypt(text: string, key: Buffer): { iv: string; encrypted: string } { const iv = randomBytes(16); const cipher = createCipheriv('aes-256-gcm', key, iv); const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); return { iv: iv.toString('hex'), encrypted: encrypted.toString('hex') }; } decrypt(encrypted: string, key: Buffer, iv: string): string { const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex')); return decipher.update(Buffer.from(encrypted, 'hex'), null, 'utf8'); } }
Secrets Management
Secrets should never be hardcoded; use environment variables, secret managers (AWS Secrets Manager, HashiCorp Vault), or encrypted configuration files with proper access controls.
import { ConfigModule, ConfigService } from '@nestjs/config'; import { SecretsManager } from '@aws-sdk/client-secrets-manager'; @Module({ imports: [ ConfigModule.forRoot({ load: [async () => { const client = new SecretsManager({ region: 'us-east-1' }); const { SecretString } = await client.getSecretValue({ SecretId: 'prod/myapp/secrets' }); return JSON.parse(SecretString); }], }), ], }) export class AppModule {} // Access secrets @Injectable() export class DbService { constructor(private config: ConfigService) { const dbPassword = this.config.get<string>('DB_PASSWORD'); } }
┌─────────────────────────────────────────────────┐
│ SECRETS HIERARCHY │
├─────────────────────────────────────────────────┤
│ 1. Vault/Secrets Manager (production) │
│ 2. Environment Variables (containers) │
│ 3. .env.local (local dev only) │
│ ❌ .env in git (NEVER) │
│ ❌ Hardcoded (NEVER) │
└─────────────────────────────────────────────────┘
Security Headers
Security headers instruct browsers to enable protections against common attacks; use Helmet middleware to set headers like CSP, HSTS, X-Frame-Options, and X-Content-Type-Options.
import helmet from 'helmet'; // main.ts app.use(helmet({ hsts: { maxAge: 31536000, includeSubDomains: true }, frameguard: { action: 'deny' }, noSniff: true, xssFilter: true, referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, })); // Response Headers: // Strict-Transport-Security: max-age=31536000; includeSubDomains // X-Frame-Options: DENY // X-Content-Type-Options: nosniff // X-XSS-Protection: 1; mode=block // Content-Security-Policy: default-src 'self'
Input Sanitization
Input sanitization removes or escapes potentially dangerous characters from user input, complementing validation to prevent injection attacks and ensure data integrity.
import { Transform } from 'class-transformer'; import * as validator from 'validator'; // Custom sanitization decorators export function Trim() { return Transform(({ value }) => typeof value === 'string' ? value.trim() : value); } export function SanitizeHtml() { return Transform(({ value }) => validator.escape(value)); } export function NormalizeEmail() { return Transform(({ value }) => validator.normalizeEmail(value) || value); } export class CreatePostDto { @IsString() @Trim() @SanitizeHtml() title: string; @IsEmail() @NormalizeEmail() authorEmail: string; } // Input: { title: " <script>alert('xss')</script> " } // Output: { title: "<script>alert('xss')</script>" }
Audit Logging
Audit logging records who did what and when, creating an immutable trail of security-relevant events for compliance, forensics, and detecting suspicious activity.
@Injectable() export class AuditInterceptor implements NestInterceptor { constructor(private auditService: AuditService) {} intercept(context: ExecutionContext, next: CallHandler) { const request = context.switchToHttp().getRequest(); const { method, url, user, ip, body } = request; return next.handle().pipe( tap({ next: (response) => { this.auditService.log({ userId: user?.id, action: `${method} ${url}`, ip, requestBody: this.redactSensitive(body), responseStatus: 'success', timestamp: new Date(), }); }, error: (error) => { this.auditService.log({ userId: user?.id, action: `${method} ${url}`, error: error.message, responseStatus: 'failure', timestamp: new Date(), }); }, }), ); } } // Audit log entry: // { userId: 123, action: "DELETE /users/456", ip: "1.2.3.4", timestamp: "..." }