Voltar para o blog
NestJSBackendArquitetura

Guards, Interceptors e Pipes no NestJS: quando usar cada um

23 de maio de 20268 min de leiturapor Sávio Araújo

O ciclo de vida de um request no NestJS

Antes de chegar no seu handler, um request passa por várias camadas em ordem bem definida:

Request → Middleware → Guards → Interceptors → Pipes → Handler → Interceptors (response) → Response

Cada camada tem uma responsabilidade específica. Usá-las no lugar certo torna o código previsível e reutilizável.

Guards: quem pode acessar

Guards respondem à pergunta: esse request tem permissão de prosseguir?

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}
 
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);
 
    if (!token) throw new UnauthorizedException("Token ausente");
 
    try {
      const payload = this.jwtService.verify(token);
      request.user = payload; // disponibiliza para o handler
      return true;
    } catch {
      throw new UnauthorizedException("Token inválido");
    }
  }
 
  private extractToken(request: Request): string | null {
    const [type, token] = request.headers.authorization?.split(" ") ?? [];
    return type === "Bearer" ? token : null;
  }
}

Guards não transformam o request: apenas permitem ou bloqueiam. Se o Guard retorna false ou lança uma exceção, o request para.

Guard de roles

export const Roles = (...roles: string[]) => SetMetadata("roles", roles);
 
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
 
  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>("roles", [
      context.getHandler(),
      context.getClass(),
    ]);
 
    if (!requiredRoles) return true;
 
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}
 
// Uso:
@Get("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles("admin")
getAdminData() {}

Pipes: transformar e validar input

Pipes respondem à pergunta: esse dado é válido e está no formato correto?

@Injectable()
export class ParseUUIDPipe implements PipeTransform {
  transform(value: unknown, metadata: ArgumentMetadata) {
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
 
    if (typeof value !== "string" || !uuidRegex.test(value)) {
      throw new BadRequestException(`${metadata.data} deve ser um UUID válido`);
    }
 
    return value.toLowerCase(); // transforma para minúsculas
  }
}
 
// Uso:
@Get(":id")
findOne(@Param("id", ParseUUIDPipe) id: string) {}

O ValidationPipe global (recomendado sempre ativar) valida DTOs automaticamente:

// main.ts
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,        // remove propriedades não declaradas no DTO
    forbidNonWhitelisted: true, // rejeita se tiver propriedades extras
    transform: true,        // transforma string para number, etc.
  })
);

Com transform: true, um @Param("id") id: number automaticamente converte a string da URL para número.

Interceptors: envolver a execução

Interceptors são mais poderosos: executam antes e depois do handler, podem transformar a resposta, cachear resultados, medir tempo, logar:

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);
 
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const request = context.switchToHttp().getRequest();
    const start = Date.now();
 
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;
        this.logger.log(`${request.method} ${request.url} → ${duration}ms`);
      }),
      catchError((error) => {
        const duration = Date.now() - start;
        this.logger.error(`${request.method} ${request.url} → ${duration}ms: ${error.message}`);
        return throwError(() => error);
      })
    );
  }
}

Interceptor de cache

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(private readonly cacheService: CacheService) {}
 
  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<unknown>> {
    const request = context.switchToHttp().getRequest();
 
    if (request.method !== "GET") return next.handle();
 
    const key = `cache:${request.url}`;
    const cached = await this.cacheService.get(key);
    if (cached) return of(cached);
 
    return next.handle().pipe(
      tap(async (data) => {
        await this.cacheService.set(key, data, 300); // 5 minutos
      })
    );
  }
}

Transformar resposta com SerializeInterceptor

@Injectable()
export class SerializeInterceptor implements NestInterceptor {
  constructor(private readonly dto: new (...args: unknown[]) => unknown) {}
 
  intercept(_: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle().pipe(
      map((data) => plainToInstance(this.dto, data, { excludeExtraneousValues: true }))
    );
  }
}
 
// Decorator personalizado:
export const Serialize = (dto: unknown) =>
  UseInterceptors(new SerializeInterceptor(dto as new () => unknown));
 
// Uso:
@Get(":id")
@Serialize(UserResponseDto)
findOne(@Param("id") id: string) {}

Middleware: o que fica fora

Middleware executa antes de tudo e é o lugar certo para:

  • Logging de requests
  • Parsing de headers customizados (ex: X-Tenant-ID)
  • Rate limiting por IP
  • Request ID generation
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    req.headers["x-request-id"] ??= crypto.randomUUID();
    res.setHeader("X-Request-ID", req.headers["x-request-id"]);
    next();
  }
}

Middleware não tem acesso ao contexto de execução do NestJS: use Guards ou Interceptors se precisar de metadados do decorator.

Quando usar cada um: resumo

| Necessidade | Use | |---|---| | Autenticação / autorização | Guard | | Validação de input | Pipe | | Transformação de input | Pipe | | Logging de request/response | Interceptor | | Cache de resposta | Interceptor | | Transformação de resposta | Interceptor | | Headers HTTP globais | Middleware | | Request ID, parsing de tenant | Middleware |

Exception Filters: o que não cabe nos outros

Exception Filters capturam erros não tratados e formatam a resposta de erro:

@Catch(PrismaClientKnownRequestError)
export class PrismaExceptionFilter implements ExceptionFilter {
  catch(exception: PrismaClientKnownRequestError, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse<Response>();
 
    if (exception.code === "P2002") {
      return response.status(409).json({
        statusCode: 409,
        error: "Conflict",
        message: "Registro já existe",
      });
    }
 
    response.status(500).json({
      statusCode: 500,
      error: "Internal Server Error",
      message: "Erro interno",
    });
  }
}

Conclusão

A clareza sobre quando usar cada abstração é o que separa um NestJS bem arquitetado de um cheio de lógica misturada. Guards respondem "pode?", Pipes respondem "é válido?", Interceptors envolvem a execução. Cada um no seu lugar, o código fica previsível e fácil de testar.

Quer conversar sobre esse tema?

Se você tem dúvidas, quer aplicar isso num projeto ou só quer trocar uma ideia, pode me chamar.

Falar com a SA Tech