Skip to main content
Advertisement

14.6 NestJS Advanced Patterns — Exception Filters, Health Checks, Testing

Custom Exception Filter

// common/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common'
import { Request, Response } from 'express'

interface ErrorResponse {
statusCode: number
message: string | string[]
error: string
path: string
timestamp: string
}

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name)

catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception.getStatus()
const exceptionResponse = exception.getResponse()

const errorResponse: ErrorResponse = {
statusCode: status,
message:
typeof exceptionResponse === 'object' && 'message' in exceptionResponse
? (exceptionResponse as { message: string | string[] }).message
: exception.message,
error: exception.name,
path: request.url,
timestamp: new Date().toISOString(),
}

this.logger.error(
`${request.method} ${request.url} ${status}`,
JSON.stringify(errorResponse)
)

response.status(status).json(errorResponse)
}
}

// Catch all exceptions
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()

const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR

response.status(status).json({
statusCode: status,
message: exception instanceof Error ? exception.message : 'Internal server error',
path: request.url,
timestamp: new Date().toISOString(),
})
}
}

// Global registration
// main.ts
app.useGlobalFilters(new HttpExceptionFilter())

Health Check

npm install @nestjs/terminus
// health/health.controller.ts
import { Controller, Get } from '@nestjs/common'
import {
HealthCheckService,
HttpHealthIndicator,
HealthCheck,
DiskHealthIndicator,
MemoryHealthIndicator,
} from '@nestjs/terminus'
import { PrismaHealthIndicator } from './prisma.health'

@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
private disk: DiskHealthIndicator,
private memory: MemoryHealthIndicator,
private prismaHealth: PrismaHealthIndicator
) {}

@Get()
@HealthCheck()
check() {
return this.health.check([
// Check database connection
() => this.prismaHealth.pingCheck('database'),

// Check external API
() => this.http.pingCheck('api', 'https://api.example.com/health'),

// Check disk usage
() =>
this.disk.checkStorage('storage', {
path: '/',
thresholdPercent: 0.9,
}),

// Check memory
() => this.memory.checkHeap('memory_heap', 300 * 1024 * 1024),
])
}
}

// health/prisma.health.ts
import { Injectable } from '@nestjs/common'
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus'
import { PrismaService } from '../prisma/prisma.service'

@Injectable()
export class PrismaHealthIndicator extends HealthIndicator {
constructor(private readonly prisma: PrismaService) {
super()
}

async pingCheck(key: string): Promise<HealthIndicatorResult> {
try {
await this.prisma.$queryRaw`SELECT 1`
return this.getStatus(key, true)
} catch (error) {
throw new HealthCheckError(
'Prisma check failed',
this.getStatus(key, false)
)
}
}
}

Unit Testing

// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
import { UsersRepository } from './users.repository'
import { NotFoundException, ConflictException } from '@nestjs/common'

// Mock Repository
const mockUsersRepository = {
findById: jest.fn(),
findByEmail: jest.fn(),
findAll: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
count: jest.fn(),
}

describe('UsersService', () => {
let service: UsersService

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: UsersRepository, useValue: mockUsersRepository },
],
}).compile()

service = module.get<UsersService>(UsersService)
jest.clearAllMocks()
})

describe('findOne', () => {
it('should return user without password when user exists', async () => {
const mockUser = {
id: '1',
name: 'Alice',
email: 'alice@test.com',
password: 'hashed',
role: 'USER',
createdAt: new Date(),
updatedAt: new Date(),
}
mockUsersRepository.findById.mockResolvedValue(mockUser)

const result = await service.findOne('1')

expect(result).not.toHaveProperty('password')
expect(result.email).toBe('alice@test.com')
})

it('should throw NotFoundException when user not found', async () => {
mockUsersRepository.findById.mockResolvedValue(null)

await expect(service.findOne('999')).rejects.toThrow(NotFoundException)
})
})

describe('create', () => {
it('should throw ConflictException on duplicate email', async () => {
mockUsersRepository.findByEmail.mockResolvedValue({ id: '1' })

await expect(
service.create({ name: 'Bob', email: 'bob@test.com', password: '12345678' })
).rejects.toThrow(ConflictException)
})
})
})

E2E Testing

// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from '../src/app.module'

describe('UsersController (e2e)', () => {
let app: INestApplication
let authToken: string

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile()

app = moduleFixture.createNestApplication()
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }))
await app.init()

// Get test token
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@test.com', password: 'password123' })
authToken = response.body.accessToken
})

afterAll(async () => {
await app.close()
})

it('GET /users — list users', () => {
return request(app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect(res => {
expect(res.body).toHaveProperty('data')
expect(Array.isArray(res.body.data)).toBe(true)
})
})

it('POST /users — invalid data', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'A', email: 'invalid-email' })
.expect(400)
})
})

Pro Tips

ConfigModule Setup

npm install @nestjs/config
// app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Global usage
envFilePath: '.env',
validationSchema: Joi.object({ // Validate environment variables
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().default(3000),
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().required().min(32),
}),
}),
],
})
export class AppModule {}

Auto-generate Swagger Documentation

// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'

const config = new DocumentBuilder()
.setTitle('API Documentation')
.setDescription('NestJS API')
.setVersion('1.0')
.addBearerAuth()
.build()

const document = SwaggerModule.createDocument(app, config)
SwaggerModule.setup('api-docs', app, document)
// View Swagger UI at http://localhost:3000/api-docs
Advertisement