NestJS dễ làm người mới ngợp vì nhìn đâu cũng thấy decorator: controller, provider, module, guard, pipe, interceptor. Nhưng nếu bóc theo đường đi của một request, 8 khái niệm này khá dễ đặt vào đúng chỗ.

Sau phần này, bạn nên đọc code NestJS đỡ lạc hơn: request vào đâu, business logic nằm ở đâu, validation chạy lúc nào, và auth chặn ở tầng nào.

Tổng quan về kiến trúc NestJS

Theo tài liệu chính thức của NestJS, có 8 khái niệm cơ bản:

  • Controllers
  • Providers
  • Modules
  • Middleware
  • Exception Filters
  • Pipes
  • Guards
  • Interceptors

Ba khái niệm đầu tiên (Controllers, Providers, Modules) đảm nhiệm các nhiệm vụ liên quan đến routing các request từ client cũng như xử lý business logic.

Năm khái niệm còn lại đều liên quan đến đường đi của request và response, được minh họa trong sơ đồ dưới đây:


  flowchart LR
    Client[Client] --> Middleware[Middleware]
    Middleware --> Guard[Guard]
    Guard --> InterceptorBefore[Interceptor trước handler]
    InterceptorBefore --> Pipe[Pipe]
    Pipe --> Controller[Controller]
    Controller --> Provider[Provider / Service]
    Provider --> InterceptorAfter[Interceptor sau handler]
    InterceptorAfter --> Response[Response]
    Controller -. lỗi .-> Filter[Exception Filter]
    Filter --> Response

Các đường chấm màu ghi là HTTP request, HTTP response và Exception (trong thực tế, Exception cũng được trả về dưới hình thức HTTP response). App Module là root module chứa các modules con, và trong mỗi module con sẽ có các controller và service.

Trên đường đi của Request sẽ lần lượt đi qua:

  • Middleware
  • Guard
  • Interceptor
  • Pipe

Còn với Response sẽ đi qua:

  • Interceptor
  • Exception Filter (trong trường hợp xảy ra Exception)

Khi đăng ký (Exception filter, Pipe, Guard, Interceptor) với app, ta có 4 cấp độ:

  • Global
  • Controller
  • Method
  • Param

Giờ đi từng khái niệm theo đúng vai trò của nó trong flow này.

1. Controllers

Controllers chịu trách nhiệm xử lý các incoming requests và trả về responses cho client. Mỗi controller có thể có nhiều route, mỗi route thực hiện một chức năng cụ thể.

@Controller("cats")
export class CatsController {
  @Get()
  findAll(): string {
    return "This action returns all cats";
  }

  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return "This action adds a new cat";
  }
}

2. Providers

Providers là khái niệm cơ bản trong NestJS. Nhiều class cơ bản trong NestJS có thể được coi là provider: services, repositories, factories, helpers, v.v. Ý tưởng chính của provider là nó có thể được inject như một dependency.

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

3. Modules

Modules là cách NestJS tổ chức ứng dụng thành các khối chức năng. Mỗi ứng dụng NestJS có ít nhất một module, gọi là root module. Module là một cách hiệu quả để tổ chức các components trong ứng dụng.

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

4. Middleware

Middleware là một function được gọi trước route handler. Middleware có quyền truy cập vào request và response objects, và có thể thực hiện các tác vụ như:

  • Thực thi code
  • Thay đổi request và response objects
  • Kết thúc request-response cycle
  • Gọi middleware tiếp theo trong stack
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log("Request...");
    next();
  }
}

Middleware có thể được áp dụng cho một route cụ thể hoặc toàn bộ ứng dụng:

// Áp dụng cho một module cụ thể
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(CatsController);
  }
}

// Áp dụng cho toàn bộ ứng dụng
const app = await NestFactory.create(AppModule);
app.use(LoggerMiddleware);

5. Exception Filters

Exception filters xử lý tất cả các exception không được xử lý trong ứng dụng. Khi một exception không được xử lý, filter sẽ bắt nó và trả về một response thích hợp.

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    // Chỉnh sửa response
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

Exception filters có thể được áp dụng ở cấp độ method, controller hoặc global:

// Cấp độ method
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  // ...
}

// Cấp độ controller
@UseFilters(HttpExceptionFilter)
export class CatsController {}

// Cấp độ global
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(HttpExceptionFilter);

6. Pipes

Pipes có hai mục đích chính:

  • Transformation: chuyển đổi input data thành dạng mong muốn
  • Validation: kiểm tra input data và throw exception nếu không hợp lệ
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException("Validation failed");
    }
    return val;
  }
}

Pipes có thể được áp dụng ở cấp độ param, method, controller hoặc global:

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id) {
  return this.catsService.findOne(id);
}

7. Guards

Guards xác định xem một request có được xử lý bởi route handler hay không, dựa trên các điều kiện nhất định (như permissions, roles, ACLs, v.v.) tại runtime. Guards thường được sử dụng để xử lý authentication và authorization.

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

Guards có thể được áp dụng ở cấp độ method, controller hoặc global:

@UseGuards(AuthGuard)
@Controller("cats")
export class CatsController {}

8. Interceptors

Interceptors là một class được annotated với @Injectable() decorator và implements NestInterceptor interface. Interceptors có nhiều khả năng:

  • Bind extra logic trước/sau khi method execution
  • Transform kết quả trả về từ function
  • Transform exception được thrown từ function
  • Extend hành vi cơ bản của function
  • Override function tùy thuộc vào điều kiện cụ thể
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log("Before...");
    const now = Date.now();
    return next
      .handle()
      .pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
  }
}

Interceptors có thể được áp dụng ở cấp độ method, controller hoặc global:

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

8 khái niệm cộng lại tạo nên kiến trúc hoàn chỉnh của NestJS

NestJS mạnh ở chỗ nó ép app đi theo một kiến trúc khá rõ: controller nhận request, provider xử lý nghiệp vụ, module gom dependency, còn middleware/filter/pipe/guard/interceptor đứng quanh request lifecycle.

Khi đã đặt được từng khái niệm vào đúng vị trí, NestJS bớt giống một rừng decorator hơn nhiều. Lúc đó câu hỏi không còn là “dùng decorator nào”, mà là “logic này thuộc tầng nào trong request flow”.


Tham khảo