NestJS là một framework Node.js mạnh mẽ được xây dựng trên TypeScript, lấy cảm hứng từ Angular và kết hợp các nguyên tắc OOP (Object Oriented Programming), FP (Functional Programming) và FRP (Functional Reactive Programming). Framework này đang ngày càng phổ biến trong cộng đồng phát triển backend nhờ vào kiến trúc module rõ ràng và khả năng mở rộng cao.

Trong bài viết này, chúng ta sẽ tìm hiểu về 8 khái niệm cơ bản trong NestJS mà mọi developer nên biết.

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:

Kiến trúc NestJS

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

Hãy cùng tìm hiểu chi tiết về từng khái niệm.

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 {}

Kết luận

NestJS cung cấp một kiến trúc rõ ràng và mạnh mẽ cho các ứng dụng Node.js. Bằng cách hiểu và sử dụng đúng 8 khái niệm cơ bản này, bạn có thể xây dựng các ứng dụng backend có khả năng mở rộng, dễ bảo trì và tuân thủ các nguyên tắc thiết kế phần mềm tốt.

Mỗi khái niệm đều có vai trò riêng trong kiến trúc NestJS và kết hợp với nhau tạo nên một framework hoàn chỉnh, đáp ứng được các yêu cầu phức tạp của các ứng dụng enterprise hiện đại.

Hy vọng bài viết này giúp bạn hiểu rõ hơn về các khái niệm cơ bản trong NestJS. Hãy thử áp dụng chúng vào dự án của bạn để thấy sự khác biệt!