Introdução ao Middleware de NestJS
NestJS é um framework robusto construído sobre Express ou Fastify que oferece uma arquitetura opinada e modular para construir aplicações Node.js escaláveis. Um dos pilares dessa arquitetura é o sistema de middleware, que permite interceptar e processar requisições em diferentes camadas da aplicação. Neste artigo, exploraremos quatro componentes fundamentais: Guards, Interceptors, Pipes e Exception Filters. Cada um deles resolve um problema específico no fluxo de requisição-resposta.
O entendimento profundo desses mecanismos é essencial para construir aplicações seguras, resilientes e bem estruturadas. Diferentemente de um simples middleware Express, esses componentes oferecem granularidade e integração com o sistema de injeção de dependências do NestJS, permitindo reutilização, testabilidade e organização do código em níveis superiores.
Guards: Controle de Acesso e Autorização
O que é um Guard e por que usá-lo?
Um Guard é um componente que decide se uma requisição deve ser processada ou não. Sua responsabilidade é determinar se o usuário tem permissão para acessar um determinado recurso ou executar uma ação específica. Guards são executados antes dos Interceptors e Pipes, o que os torna ideais para validações de autenticação e autorização.
A principal diferença entre um Guard e um middleware tradicional é que Guards têm acesso ao contexto de execução (ExecutionContext), que fornece informações detalhadas sobre a requisição, o controller e o handler que será executado. Isso permite tomar decisões mais informadas sobre controle de acesso.
Implementando um Guard de Autenticação
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('Token não fornecido');
}
try {
const payload = this.jwtService.verify(token);
request.user = payload;
return true;
} catch (error) {
throw new UnauthorizedException('Token inválido ou expirado');
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Neste exemplo, o Guard extrai o token JWT do header Authorization, valida-o e, se válido, adiciona as informações do usuário ao objeto request. Se o token não existir ou for inválido, lança uma exceção. O método canActivate retorna true para permitir o acesso ou lança uma exceção para negá-lo.
Guard de Autorização baseado em Roles
Guards também podem ser usados para verificar permissões específicas do usuário. Veja como implementar um Guard que verifica se o usuário possui um determinado papel:
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export const ROLES_KEY = 'roles';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>(ROLES_KEY, context.getHandler());
// Se não há roles definidas, permite acesso
if (!requiredRoles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new ForbiddenException('Usuário não autenticado');
}
const hasRole = requiredRoles.some(role => user.roles?.includes(role));
if (!hasRole) {
throw new ForbiddenException('Você não tem permissão para acessar este recurso');
}
return true;
}
}
Para usar este Guard, você cria um decorator customizado:
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
E então aplica no seu controller:
@Controller('admin')
export class AdminController {
@Post('users')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin', 'moderator')
createUser(@Body() createUserDto: CreateUserDto) {
return { message: 'Usuário criado com sucesso' };
}
}
Interceptors: Transformação e Logging
Entendendo Interceptors
Interceptors são componentes que envolvem toda a lógica de um handler, permitindo executar código antes e depois de sua execução. Diferentemente de Guards, que simplesmente permitem ou negam acesso, Interceptors podem transformar a requisição, a resposta, ou ambas. São úteis para logging, cache, tratamento de erros e transformação de dados.
Um Interceptor implementa a interface NestInterceptor e trabalha com o padrão RxJS Observable, o que oferece poder e flexibilidade para lidar com fluxos assíncronos de forma elegante.
Implementando um Interceptor de Logging
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, body } = request;
const startTime = Date.now();
this.logger.log(`Iniciando ${method} ${url}`);
return next.handle().pipe(
tap((response) => {
const duration = Date.now() - startTime;
this.logger.log(`Finalizado ${method} ${url} em ${duration}ms`);
}),
);
}
}
Este Interceptor registra a hora de início da requisição, deixa a requisição passar, e após a conclusão, registra o tempo decorrido. O next.handle() retorna um Observable que representa a execução do handler, e usamos o operador RxJS tap para executar efeitos colaterais sem modificar o fluxo.
Interceptor para Transformação de Resposta
Interceptors também podem transformar a resposta padrão. Por exemplo, envolver sempre a resposta em um objeto padrão:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface ResponseFormat<T> {
statusCode: number;
message: string;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ResponseFormat<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ResponseFormat<T>> {
const response = context.switchToHttp().getResponse();
const statusCode = response.statusCode || 200;
return next.handle().pipe(
map((data) => ({
statusCode,
message: 'Sucesso',
data,
timestamp: new Date().toISOString(),
})),
);
}
}
Aplicar o Interceptor globalmente no main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './interceptors/transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();
Pipes: Validação e Transformação de Dados
O Propósito dos Pipes
Pipes são componentes que transformam dados de entrada e realizam validação antes que esses dados cheguem ao handler. Eles executam depois dos Guards mas antes do Interceptor processar a requisição. Pipes são essenciais para garantir que os dados recebidos estão no formato correto e contêm valores válidos, criando uma barreira de segurança e qualidade de dados.
NestJS fornece vários pipes built-in como ValidationPipe, ParseIntPipe, ParseUUIDPipe, mas também permite criar pipes customizados para validações específicas do seu domínio.
Usando o ValidationPipe com Class Validator
Para validação robusta, combine class-validator e class-transformer:
npm install class-validator class-transformer
Crie um DTO:
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsString()
@IsOptional()
firstName?: string;
}
Aplique o ValidationPipe no controller:
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
createUser(@Body() createUserDto: CreateUserDto) {
return { message: 'Usuário criado', data: createUserDto };
}
}
A opção whitelist: true remove propriedades que não estão definidas no DTO, enquanto forbidNonWhitelisted: true lança um erro se propriedades extras forem fornecidas.
Implementando um Pipe Customizado
Às vezes, você precisa de validações mais específicas. Aqui está um Pipe que valida se um valor é um número positivo:
import { Injectable, PipeTransform, BadRequestException, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ParsePositiveIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const intValue = parseInt(value, 10);
if (isNaN(intValue)) {
throw new BadRequestException(`${metadata.data} deve ser um número válido`);
}
if (intValue <= 0) {
throw new BadRequestException(`${metadata.data} deve ser um número positivo`);
}
return intValue;
}
}
Usando o pipe customizado:
@Controller('products')
export class ProductsController {
@Get(':id')
getProduct(@Param('id', ParsePositiveIntPipe) id: number) {
return { id, name: 'Produto Exemplo' };
}
}
Exception Filters: Tratamento Centralizado de Erros
O Papel dos Exception Filters
Exception Filters são responsáveis por capturar exceções não tratadas durante a execução de um handler e formatar uma resposta apropriada ao cliente. Eles garantem que erros sejam comunicados de forma consistente e segura, sem expor detalhes internos da aplicação. Um bom tratamento de exceções melhora significativamente a experiência do cliente e facilita debugging.
NestJS fornece exceções built-in como BadRequestException, UnauthorizedException, NotFoundException, mas você também pode criar filtros customizados para cenários específicos.
Implementando um Exception Filter Customizado
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const errorMessage = typeof exceptionResponse === 'object'
? (exceptionResponse as any).message
: exceptionResponse;
this.logger.error(
`${request.method} ${request.url} - ${status} - ${JSON.stringify(errorMessage)}`,
);
response.status(status).json({
statusCode: status,
message: errorMessage,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
Aplique globalmente no main.ts:
app.useGlobalFilters(new HttpExceptionFilter());
Exception Filter para Erros Não Esperados
Às vezes, exceções que não herdam de HttpException são lançadas. Capture-as também:
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus, Logger } from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Erro interno do servidor';
if (exception instanceof Error) {
this.logger.error(`${exception.name}: ${exception.message}`, exception.stack);
message = exception.message;
} else {
this.logger.error(exception);
}
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
Exception Customizada do Domínio
Para casos específicos da sua aplicação, crie exceções customizadas:
import { HttpException, HttpStatus } from '@nestjs/common';
export class InsufficientFundsException extends HttpException {
constructor(message: string = 'Saldo insuficiente') {
super(
{
statusCode: HttpStatus.PAYMENT_REQUIRED,
message,
code: 'INSUFFICIENT_FUNDS',
},
HttpStatus.PAYMENT_REQUIRED,
);
}
}
E um filter específico para ela:
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Response } from 'express';
import { InsufficientFundsException } from './exceptions/insufficient-funds.exception';
@Catch(InsufficientFundsException)
export class InsufficientFundsFilter implements ExceptionFilter {
catch(exception: InsufficientFundsException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const exceptionResponse = exception.getResponse();
response.status(exception.getStatus()).json({
...exceptionResponse,
timestamp: new Date().toISOString(),
});
}
}
Ordem de Execução e Integração
Fluxo Completo de uma Requisição
Para entender completamente como esses componentes funcionam juntos, é importante conhecer a ordem de execução:
- Middleware Global (Express) - primeiro nível de processamento
- Guards - validação de acesso (retorna true/false ou lança exceção)
- Pipes - validação e transformação de dados de entrada
- Interceptors (antes do handler) - logging, cache, etc.
- Handler - execução da lógica principal
- Interceptors (depois do handler) - transformação de resposta
- Exception Filters - tratamento de qualquer exceção lançada em qualquer etapa anterior
Exemplo Prático Integrado
Aqui está um exemplo completo mostrando todos os componentes funcionando em conjunto:
import {
Controller,
Post,
Body,
UseGuards,
UseInterceptors,
UsePipes,
ValidationPipe,
UseFilters,
} from '@nestjs/common';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RolesGuard, Roles } from './guards/roles.guard';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { TransformInterceptor } from './interceptors/transform.interceptor';
import { CreateUserDto } from './dto/create-user.dto';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { UsersService } from './users.service';
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
@UseFilters(HttpExceptionFilter)
export class UsersController {
constructor(private usersService: UsersService) {}
@Post()
@Roles('admin')
@UsePipes(new ValidationPipe({ whitelist: true }))
async createUser(@Body() createUserDto: CreateUserDto) {
const user = await this.usersService.create(createUserDto);
return user;
}
}
Neste exemplo, quando uma requisição chega:
1. O JwtAuthGuard valida se há um token válido
2. O RolesGuard verifica se o usuário tem o papel 'admin'
3. O ValidationPipe valida os dados do DTO
4. O LoggingInterceptor registra o início da requisição
5. O handler executa a lógica de criação do usuário
6. O TransformInterceptor formata a resposta
7. Se qualquer erro ocorrer, o HttpExceptionFilter o trata
Conclusão
Dominar Guards, Interceptors, Pipes e Exception Filters é essencial para construir aplicações NestJS robustas e profissionais. Primeiro, Guards oferecem uma camada de segurança permitindo controlar quem pode acessar recursos, enquanto Pipes garantem que os dados estejam válidos antes de chegar ao handler. Segundo, Interceptors fornecem um mecanismo elegante para adicionar comportamentos transversais como logging e transformação de dados sem poluir a lógica principal. Terceiro, Exception Filters centralizam o tratamento de erros, garantindo consistência nas respostas e facilitando debugging, tornando sua API mais previsível e confiável.