Как работает цепочка обработки запроса в NestJS?

Ответ

В NestJS запрос проходит через строго определенную цепочку (middleware -> guards -> interceptors -> pipes -> controller -> interceptors -> exception filters). Я настраивал эту цепочку для добавления логирования, авторизации и валидации.

Порядок выполнения:

  1. Глобальные и модульные Middleware: Идеально для логирования, CORS, сжатия данных. Они выполняются до маршрутизации.
  2. Guards: Проверяют права доступа на основе ролей или JWT-токена. Если guard возвращает false, цепочка прерывается.
  3. Interceptors (до обработчика): Могут трансформировать входящий запрос или добавлять дополнительную логику (например, логирование времени выполнения).
  4. Pipes: Преобразуют и валидируют данные для параметров обработчика (например, с помощью class-validator).
  5. Controller Handler: Выполняется целевой метод контроллера.
  6. Interceptors (после обработчика): Могут трансформировать исходящий ответ.
  7. Exception Filters: Перехватывают и обрабатывают исключения, возникшие на любом этапе.

Пример Guard и Pipe:

// Guard для проверки JWT
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];
    if (!token) throw new UnauthorizedException();

    try {
      const payload = this.jwtService.verify(token);
      request.user = payload; // Добавляем пользователя в запрос
      return true;
    } catch {
      throw new UnauthorizedException('Неверный токен');
    }
  }
}

// Pipe для валидации DTO
@Injectable()
export class ValidationPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const object = plainToClass(metadata.metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Валидация не пройдена');
    }
    return object;
  }
}

Практическое применение: В моем последнем проекте я использовал глобальный interceptor для оборачивания всех ответов в единый формат { data: ..., timestamp: ... } и глобальный guard для проверки API-ключей на публичных эндпоинтах.

Ответ 18+ 🔞

Давай разберём эту матрёшку из NestJS, а то звучит как инструкция к стиральной машине, которую писал человек с овердохуищей свободного времени.

Смотри, представь, что запрос — это какой-нибудь Вася, который пришёл в закрытый клуб. Порядок, в котором его будут обламывать, строгий и неумолимый.

  1. Middleware (Глобальные и модульные). Это как швейцар у двери и фейсконтроль в одном флаконе. Вася только переступил порог, а его уже оценивают: «А, это опять ты, логируем. Разрешаем пройти дальше, но CORS тебе в сраку, чувак». Выполняется до того, как вообще поняли, в какой зал ему идти.
  2. Guards. Вот Вася подошёл к нужной двери (роуту). А там стоит здоровенный чувак с лицом кирпичом и спрашивает: «А у тебя есть токен? А роль подходящая?». Это Guard. Если нет — «Иди нахуй, Вася», и вся история на этом заканчивается. Авторизация, JWT — всё тут.
  3. Interceptors (до обработчика). Допустим, Guard пропустил. Но прежде чем Вася зайдёт в зал (контроллер), его могут ещё раз обыскать или, например, засечь время входа. Interceptor — это такой хитрожопый администратор, который может покрутить запрос в руках, что-то подкрутить, залогировать «Вася зашёл в 21:37».
  4. Pipes. Вася внутри, хочет заказать напиток. Но говорит: «Дайте мне аджыку». Pipe — это бармен, который его слушает и думает: «Бля, аджика — это соус. Наверное, он имел в виду «Апероль». Или вообще: «Нет, чувак, такую хуйню мы не подаём, вот тебе BadRequestException». Валидация и преобразование данных — их царство. Class-validator тут рулит.
  5. Controller Handler. Наконец-то! Вася дошёл до стойки, сделал заказ (вызвался метод контроллера), и ему налили его напиток (выполнили бизнес-логику).
  6. Interceptors (после обработчика). Вася получил свой коктейль и поворачивается уходить. А тот самый хитрожопый администратор (interceptor) снова тут как тут: «Погоди, Вася. Давай я твой стакан красиво упакую в обёртку { data: твой_коктейль, timestamp: ... }, а то выходить с голым стаканом — моветон». Трансформация ответа.
  7. Exception Filters. А вот если на любом из этих этапов всё пошло по пизде — Васи нет в списке, у Васи токен просрочен, Вася заказал аджику, а бармен обозвался и швырнул в него стаканом — за всё это отвечают Exception Filters. Они ловят эти летящие нахуй исключения и говорят: «Так, народ, не кипятиться. Васе мы сейчас красиво отправим 401 или 400, чтобы он не охуел совсем».

Вот тебе примеры, как это выглядит в коде, ёпта:

// Guard — тот самый бугай на входе. Проверяет JWT.
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    // Ищем токен в заголовках. Если его нет — сразу иди нахуй.
    const token = request.headers.authorization?.split(' ')[1];
    if (!token) throw new UnauthorizedException();

    try {
      // Пробуем проверить. Не верим на слово.
      const payload = this.jwtService.verify(token);
      request.user = payload; // Если всё ок — прилепляем пользователя к запросу.
      return true;
    } catch {
      // Токен кривой или просроченный? Иди нахуй, Вася, в другом месте кодируй.
      throw new UnauthorizedException('Неверный токен');
    }
  }
}

// Pipe — бармен-педант. Валидирует DTO.
@Injectable()
export class ValidationPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    // Превращаем сырые данные (скорее всего, объект) в красивый класс.
    const object = plainToClass(metadata.metatype, value);
    // Спрашиваем у class-validator: «Ну чё, нормально?»
    const errors = await validate(object);
    if (errors.length > 0) {
      // Если ошибок овердохуища — кидаем исключение.
      throw new BadRequestException('Валидация не пройдена');
    }
    // Если всё чисто — пропускаем дальше, в контроллер.
    return object;
  }
}

А на практике, в последнем проекте, я это применял так: Глобальный interceptor — чтобы все ответы от сервера были завёрнуты в одну обёртку, типа { data: ..., timestamp: ... }. А то фронтендеры охуевают, когда у каждого эндпоинта свой формат. И глобальный guard — который на публичных эндпоинтах проверял не пользователя, а API-ключи. Типа «ты кто такой, давай, предъяви ключ, а то доверия к тебе — ноль ебать». Удобно, чёрт возьми. Всё на своих местах, как в хорошем механизме.