Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
40 prioritized NestJS best practices across architecture, DI, security, performance, testing, and microservices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/api-use-interceptors.md
1---2title: Use Interceptors for Cross-Cutting Concerns3impact: MEDIUM-HIGH4impactDescription: Interceptors provide clean separation for cross-cutting logic5tags: api, interceptors, logging, caching6---78## Use Interceptors for Cross-Cutting Concerns910Interceptors can transform responses, add logging, handle caching, and measure performance without polluting your business logic. They wrap the route handler execution, giving you access to both the request and response streams.1112**Incorrect (logging and transformation in every method):**1314```typescript15// Logging in every controller method16@Controller('users')17export class UsersController {18@Get()19async findAll(): Promise<User[]> {20const start = Date.now();21this.logger.log('findAll called');2223const users = await this.usersService.findAll();2425this.logger.log(`findAll completed in ${Date.now() - start}ms`);26return users;27}2829@Get(':id')30async findOne(@Param('id') id: string): Promise<User> {31const start = Date.now();32this.logger.log(`findOne called with id: ${id}`);3334const user = await this.usersService.findOne(id);3536this.logger.log(`findOne completed in ${Date.now() - start}ms`);37return user;38}39// Repeated in every method!40}4142// Manual response wrapping43@Get()44async findAll(): Promise<{ data: User[]; meta: Meta }> {45const users = await this.usersService.findAll();46return {47data: users,48meta: { timestamp: new Date(), count: users.length },49};50}51```5253**Correct (use interceptors for cross-cutting concerns):**5455```typescript56// Logging interceptor57@Injectable()58export class LoggingInterceptor implements NestInterceptor {59private readonly logger = new Logger('HTTP');6061intercept(context: ExecutionContext, next: CallHandler): Observable<any> {62const request = context.switchToHttp().getRequest();63const { method, url, body } = request;64const now = Date.now();6566return next.handle().pipe(67tap({68next: (data) => {69const response = context.switchToHttp().getResponse();70this.logger.log(71`${method} ${url} ${response.statusCode} - ${Date.now() - now}ms`,72);73},74error: (error) => {75this.logger.error(76`${method} ${url} ${error.status || 500} - ${Date.now() - now}ms`,77error.stack,78);79},80}),81);82}83}8485// Response transformation interceptor86@Injectable()87export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {88intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {89return next.handle().pipe(90map((data) => ({91data,92meta: {93timestamp: new Date().toISOString(),94path: context.switchToHttp().getRequest().url,95},96})),97);98}99}100101// Timeout interceptor102@Injectable()103export class TimeoutInterceptor implements NestInterceptor {104intercept(context: ExecutionContext, next: CallHandler): Observable<any> {105return next.handle().pipe(106timeout(5000),107catchError((err) => {108if (err instanceof TimeoutError) {109throw new RequestTimeoutException('Request timed out');110}111throw err;112}),113);114}115}116117// Apply globally or per-controller118@Module({119providers: [120{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },121{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },122],123})124export class AppModule {}125126// Or per-controller127@Controller('users')128@UseInterceptors(LoggingInterceptor)129export class UsersController {130@Get()131async findAll(): Promise<User[]> {132// Clean business logic only133return this.usersService.findAll();134}135}136137// Custom cache interceptor with TTL138@Injectable()139export class HttpCacheInterceptor implements NestInterceptor {140constructor(141private cacheManager: Cache,142private reflector: Reflector,143) {}144145async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {146const request = context.switchToHttp().getRequest();147148// Only cache GET requests149if (request.method !== 'GET') {150return next.handle();151}152153const cacheKey = this.generateKey(request);154const ttl = this.reflector.get<number>('cacheTTL', context.getHandler()) || 300;155156const cached = await this.cacheManager.get(cacheKey);157if (cached) {158return of(cached);159}160161return next.handle().pipe(162tap((response) => {163this.cacheManager.set(cacheKey, response, ttl);164}),165);166}167168private generateKey(request: Request): string {169return `cache:${request.url}:${JSON.stringify(request.query)}`;170}171}172173// Usage with custom TTL174@Get()175@SetMetadata('cacheTTL', 600)176@UseInterceptors(HttpCacheInterceptor)177async findAll(): Promise<User[]> {178return this.usersService.findAll();179}180181// Error mapping interceptor182@Injectable()183export class ErrorMappingInterceptor implements NestInterceptor {184intercept(context: ExecutionContext, next: CallHandler): Observable<any> {185return next.handle().pipe(186catchError((error) => {187if (error instanceof EntityNotFoundError) {188throw new NotFoundException(error.message);189}190if (error instanceof QueryFailedError) {191if (error.message.includes('duplicate')) {192throw new ConflictException('Resource already exists');193}194}195throw error;196}),197);198}199}200```201202Reference: [NestJS Interceptors](https://docs.nestjs.com/interceptors)203