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.