Lesson 07-NestJS Interceptors and Decorators

Interceptors

Interceptors in NestJS v10 are a highly useful feature that allows you to execute operations before and after a controller method runs. They can be used to modify request or response objects, log information, monitor performance, and more.

Introduction to Interceptors

Interceptors are pieces of code that run before and after a controller method is executed. They can modify request or response objects or perform additional tasks such as logging or performance monitoring.

Creating Interceptors

To create an interceptor, you need to implement the NestInterceptor interface.

// src/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, CallHandler, ExecutionContext, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const logger = new Logger('LoggingInterceptor');

    return next.handle().pipe(
      tap(() => logger.log(`After... ${Date.now() - now}ms`)),
    );
  }
}

Using Interceptors

Interceptors can be applied at the method level or globally across the application.

// src/controllers/users.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './interceptors/logging.interceptor';

@Controller('users')
export class UsersController {
  @Get()
  @UseInterceptors(LoggingInterceptor)
  findAll() {
    return 'List of users';
  }
}

Global Interceptors

You can set a global interceptor to avoid declaring interceptors for each controller or method.

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new LoggingInterceptor());
  await app.listen(3000);
}
bootstrap();

Local Interceptors

Local interceptors can be applied to specific controllers or methods.

// src/controllers/users.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './interceptors/logging.interceptor';

@Controller('users')
export class UsersController {
  @Get()
  @UseInterceptors(LoggingInterceptor)
  findAll() {
    return 'List of users';
  }
}

Multiple Interceptors

You can apply multiple interceptors to a single controller method.

// src/interceptors/error.interceptor.ts
import { Injectable, NestInterceptor, CallHandler, ExecutionContext, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(error => {
        const logger = new Logger('ErrorInterceptor');
        logger.error('An error occurred:', error.stack);
        throw { statusCode: 500, message: 'Internal Server Error' };
      }),
    );
  }
}

// src/controllers/users.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor, ErrorInterceptor } from './interceptors';

@Controller('users')
export class UsersController {
  @Get()
  @UseInterceptors(LoggingInterceptor, ErrorInterceptor)
  findAll() {
    return 'List of users';
  }
}

Dependency Injection in Interceptors

Interceptors can leverage dependency injection to access other services as needed.

// src/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, CallHandler, ExecutionContext, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private readonly logger: Logger) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();

    return next.handle().pipe(
      tap(() => this.logger.log(`After... ${Date.now() - now}ms`)),
    );
  }
}

Error Handling in Interceptors

When an interceptor catches an error, you can customize the error response.

// src/interceptors/error.interceptor.ts
import { Injectable, NestInterceptor, CallHandler, ExecutionContext, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(error => {
        const logger = new Logger('ErrorInterceptor');
        logger.error('An error occurred:', error.stack);
        throw { statusCode: 500, message: 'Internal Server Error' };
      }),
    );
  }
}

Custom Error Responses

You can use custom error responses to handle errors more effectively.

// src/interceptors/error.interceptor.ts
import { Injectable, NestInterceptor, CallHandler, ExecutionContext, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(error => {
        const logger = new Logger('ErrorInterceptor');
        logger.error('An error occurred:', error.stack);
        throw { statusCode: 500, message: 'Internal Server Error', error: error.message };
      }),
    );
  }
}

Logging

You can use interceptors to log request and response information.

// src/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, CallHandler, ExecutionContext, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const logger = new Logger('LoggingInterceptor');
    const request = context.switchToHttp().getRequest();

    logger.log(`Request... ${request.method} ${request.url}`);

    return next.handle().pipe(
      tap(() => logger.log(`After... ${Date.now() - now}ms`)),
    );
  }
}

Decorators

Decorators in NestJS v10 are a powerful feature that allows you to define component behavior declaratively. They can be used to mark classes, properties, methods, or parameters and add metadata to these elements.

Introduction to Decorators

Decorators are a special type of declaration that can be attached to class declarations, methods, accessors, properties, or parameters. In NestJS, decorators are primarily used to define injection metadata and route paths.

Creating Decorators

To create a decorator, you need to define a decorator function that takes a target object as a parameter and can modify the object or add metadata to it.

// src/decorators/log-method.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const LogMethod = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  console.log(`Calling method with id: ${request.params.id}`);
  return request.params.id;
});

Using Decorators

Decorators can be used on controller method parameters.

// src/controllers/users.controller.ts
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { LogMethod } from './decorators/log-method.decorator';

@Controller('users')
export class UsersController {
  @Get(':id')
  findUser(@LogMethod() id: string) {
    return `This action returns the user with id ${id}`;
  }
}

Injecting Metadata

Decorators can be used to add metadata to classes, methods, and other elements.

// src/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

Using Injected Metadata

You can use the @Roles decorator to restrict access.

// src/controllers/admin.controller.ts
import { Controller, Get, UseGuards, Roles } from '@nestjs/common';
import { AuthGuard } from './guards/auth.guard';
import { RoleGuard } from './guards/role.guard';
import { ROLES_KEY } from './decorators/roles.decorator';

@Controller('admin')
export class AdminController {
  @Get()
  @UseGuards(AuthGuard, RoleGuard)
  @Roles('admin')
  index() {
    return 'Admin page';
  }
}

Custom Decorators

You can create custom decorators to meet specific needs.

// src/decorators/log-method.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const LogMethod = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  console.log(`Calling method with id: ${request.params.id}`);
  return request.params.id;
});

Using Custom Decorators

You can use custom decorators for logging or other operations.

// src/controllers/users.controller.ts
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { LogMethod } from './decorators/log-method.decorator';

@Controller('users')
export class UsersController {
  @Get(':id')
  findUser(@LogMethod() id: string) {
    return `This action returns the user with id ${id}`;
  }
}

Route Path Decorators

NestJS provides built-in decorators to define route paths.

// src/controllers/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get(':id')
  findUser(@Param('id') id: string) {
    return `This action returns the user with id ${id}`;
  }
}

Controller Decorators

Controller decorators are used to define the route prefix for a controller.

// src/controllers/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get(':id')
  findUser(@Param('id') id: string) {
    return `This action returns the user with id ${id}`;
  }
}

Injection Decorators

Injection decorators are used to extract data from requests.

// src/controllers/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get(':id')
  findUser(@Param('id') id: string) {
    return `This action returns the user with id ${id}`;
  }
}

Parameter Decorators

Parameter decorators are used to extract data from requests.

// src/controllers/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get(':id')
  findUser(@Param('id') id: string) {
    return `This action returns the user with id ${id}`;
  }
}

Using Parameter Decorators

You can use decorators like @Param, @Body, and @Query to extract data from requests.

// src/controllers/users.controller.ts
import { Controller, Get, Param, Body, Query } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get(':id')
  findUser(@Param('id') id: string) {
    return `This action returns the user with id ${id}`;
  }

  @Post()
  createUser(@Body() user: any) {
    return `This action adds a new user: ${JSON.stringify(user)}`;
  }

  @Get()
  findAllUsers(@Query('status') status: string) {
    return `This action returns all users with status: ${status}`;
  }
}

Metadata Decorators

Metadata decorators are used to add metadata to classes, methods, and other elements.

// src/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

Using Metadata Decorators

You can use metadata decorators to restrict access.

// src/controllers/admin.controller.ts
import { Controller, Get, UseGuards, Roles } from '@nestjs/common';
import { AuthGuard } from './guards/auth.guard';
import { RoleGuard } from './guards/role.guard';
import { ROLES_KEY } from './decorators/roles.decorator';

@Controller('admin')
export class AdminController {
  @Get()
  @UseGuards(AuthGuard, RoleGuard)
  @Roles('admin')
  index() {
    return 'Admin page';
  }
}

Dependency Injection in Decorators

Decorators can leverage dependency injection to access other services as needed.

// src/decorators/log-method.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const LogMethod = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  console.log(`Calling method with id: ${request.params.id}`);
  return request.params.id;
});
Share your love