<h2>Introdução ao Middleware de NestJS</h2>
<p>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.</p>
<p>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.</p>
<h2>Guards: Controle de Acesso e Autorização</h2>
<h3>O que é um Guard e por que usá-lo?</h3>
<p>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 <em>antes</em> dos Interceptors e Pipes, o que os torna ideais para validações de autenticação e autorização.</p>
<p>A principal diferença entre um Guard e um middleware tradicional é que Guards têm acesso ao contexto de execução (<code>ExecutionContext</code>), 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.</p>
<h3>Implementando um Guard de Autenticação</h3>
<pre><code class="language-typescript">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;
}
}</code></pre>
<p>Neste exemplo, o Guard extrai o token JWT do header <code>Authorization</code>, valida-o e, se válido, adiciona as informações do usuário ao objeto <code>request</code>. Se o token não existir ou for inválido, lança uma exceção. O método <code>canActivate</code> retorna <code>true</code> para permitir o acesso ou lança uma exceção para negá-lo.</p>
<h3>Guard de Autorização baseado em Roles</h3>
<p>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:</p>
<pre><code class="language-typescript">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;
}
}</code></pre>
<p>Para usar este Guard, você cria um decorator customizado:</p>
<pre><code class="language-typescript">import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);</code></pre>
<p>E então aplica no seu controller:</p>
<pre><code class="language-typescript">@Controller('admin')
export class AdminController {
@Post('users')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin', 'moderator')
createUser(@Body() createUserDto: CreateUserDto) {
return { message: 'Usuário criado com sucesso' };
}
}</code></pre>
<h2>Interceptors: Transformação e Logging</h2>
<h3>Entendendo Interceptors</h3>
<p>Interceptors são componentes que envolvem toda a lógica de um handler, permitindo executar código <em>antes</em> e <em>depois</em> 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.</p>
<p>Um Interceptor implementa a interface <code>NestInterceptor</code> e trabalha com o padrão RxJS Observable, o que oferece poder e flexibilidade para lidar com fluxos assíncronos de forma elegante.</p>
<h3>Implementando um Interceptor de Logging</h3>
<pre><code class="language-typescript">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);
}),
);
}
}</code></pre>
<p>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 <code>next.handle()</code> retorna um Observable que representa a execução do handler, e usamos o operador RxJS <code>tap</code> para executar efeitos colaterais sem modificar o fluxo.</p>
<h3>Interceptor para Transformação de Resposta</h3>
<p>Interceptors também podem transformar a resposta padrão. Por exemplo, envolver sempre a resposta em um objeto padrão:</p>
<pre><code class="language-typescript">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(),
})),
);
}
}</code></pre>
<p>Aplicar o Interceptor globalmente no <code>main.ts</code>:</p>
<pre><code class="language-typescript">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();</code></pre>
<h2>Pipes: Validação e Transformação de Dados</h2>
<h3>O Propósito dos Pipes</h3>
<p>Pipes são componentes que transformam dados de entrada e realizam validação antes que esses dados cheguem ao handler. Eles executam <em>depois</em> dos Guards mas <em>antes</em> 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.</p>
<p>NestJS fornece vários pipes built-in como <code>ValidationPipe</code>, <code>ParseIntPipe</code>, <code>ParseUUIDPipe</code>, mas também permite criar pipes customizados para validações específicas do seu domínio.</p>
<h3>Usando o ValidationPipe com Class Validator</h3>
<p>Para validação robusta, combine <code>class-validator</code> e <code>class-transformer</code>:</p>
<pre><code class="language-bash">npm install class-validator class-transformer</code></pre>
<p>Crie um DTO:</p>
<pre><code class="language-typescript">import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsString()
@IsOptional()
firstName?: string;
}</code></pre>
<p>Aplique o <code>ValidationPipe</code> no controller:</p>
<pre><code class="language-typescript">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 };
}
}</code></pre>
<p>A opção <code>whitelist: true</code> remove propriedades que não estão definidas no DTO, enquanto <code>forbidNonWhitelisted: true</code> lança um erro se propriedades extras forem fornecidas.</p>
<h3>Implementando um Pipe Customizado</h3>
<p>Às vezes, você precisa de validações mais específicas. Aqui está um Pipe que valida se um valor é um número positivo:</p>
<pre><code class="language-typescript">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;
}
}</code></pre>
<p>Usando o pipe customizado:</p>
<pre><code class="language-typescript">@Controller('products')
export class ProductsController {
@Get(':id')
getProduct(@Param('id', ParsePositiveIntPipe) id: number) {
return { id, name: 'Produto Exemplo' };
}
}</code></pre>
<h2>Exception Filters: Tratamento Centralizado de Erros</h2>
<h3>O Papel dos Exception Filters</h3>
<p>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.</p>
<p>NestJS fornece exceções built-in como <code>BadRequestException</code>, <code>UnauthorizedException</code>, <code>NotFoundException</code>, mas você também pode criar filtros customizados para cenários específicos.</p>
<h3>Implementando um Exception Filter Customizado</h3>
<pre><code class="language-typescript">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,
});
}
}</code></pre>
<p>Aplique globalmente no <code>main.ts</code>:</p>
<pre><code class="language-typescript">app.useGlobalFilters(new HttpExceptionFilter());</code></pre>
<h3>Exception Filter para Erros Não Esperados</h3>
<p>Às vezes, exceções que não herdam de <code>HttpException</code> são lançadas. Capture-as também:</p>
<pre><code class="language-typescript">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,
});
}
}</code></pre>
<h3>Exception Customizada do Domínio</h3>
<p>Para casos específicos da sua aplicação, crie exceções customizadas:</p>
<pre><code class="language-typescript">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,
);
}
}</code></pre>
<p>E um filter específico para ela:</p>
<pre><code class="language-typescript">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(),
});
}
}</code></pre>
<h2>Ordem de Execução e Integração</h2>
<h3>Fluxo Completo de uma Requisição</h3>
<p>Para entender completamente como esses componentes funcionam juntos, é importante conhecer a ordem de execução:</p>
<ol>
<li><strong>Middleware Global</strong> (Express) - primeiro nível de processamento</li>
<li><strong>Guards</strong> - validação de acesso (retorna true/false ou lança exceção)</li>
<li><strong>Pipes</strong> - validação e transformação de dados de entrada</li>
<li><strong>Interceptors</strong> (antes do handler) - logging, cache, etc.</li>
<li><strong>Handler</strong> - execução da lógica principal</li>
<li><strong>Interceptors</strong> (depois do handler) - transformação de resposta</li>
<li><strong>Exception Filters</strong> - tratamento de qualquer exceção lançada em qualquer etapa anterior</li>
</ol>
<h3>Exemplo Prático Integrado</h3>
<p>Aqui está um exemplo completo mostrando todos os componentes funcionando em conjunto:</p>
<pre><code class="language-typescript">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;
}
}</code></pre>
<p>Neste exemplo, quando uma requisição chega:</p>
<ol>
<li>O <code>JwtAuthGuard</code> valida se há um token válido</li>
<li>O <code>RolesGuard</code> verifica se o usuário tem o papel 'admin'</li>
<li>O <code>ValidationPipe</code> valida os dados do DTO</li>
<li>O <code>LoggingInterceptor</code> registra o início da requisição</li>
<li>O handler executa a lógica de criação do usuário</li>
<li>O <code>TransformInterceptor</code> formata a resposta</li>
<li>Se qualquer erro ocorrer, o <code>HttpExceptionFilter</code> o trata</li>
</ol>
<h2>Conclusão</h2>
<p>Dominar Guards, Interceptors, Pipes e Exception Filters é essencial para construir aplicações NestJS robustas e profissionais. <strong>Primeiro</strong>, 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. <strong>Segundo</strong>, Interceptors fornecem um mecanismo elegante para adicionar comportamentos transversais como logging e transformação de dados sem poluir a lógica principal. <strong>Terceiro</strong>, Exception Filters centralizam o tratamento de erros, garantindo consistência nas respostas e facilitando debugging, tornando sua API mais previsível e confiável.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.nestjs.com/guards" target="_blank" rel="noopener noreferrer">NestJS Official Documentation - Guards</a></li>
<li><a href="https://docs.nestjs.com/interceptors" target="_blank" rel="noopener noreferrer">NestJS Official Documentation - Interceptors</a></li>
<li><a href="https://docs.nestjs.com/pipes" target="_blank" rel="noopener noreferrer">NestJS Official Documentation - Pipes</a></li>
<li><a href="https://docs.nestjs.com/exception-filters" target="_blank" rel="noopener noreferrer">NestJS Official Documentation - Exception Filters</a></li>
<li><a href="https://rxjs.dev/guide/operators" target="_blank" rel="noopener noreferrer">RxJS Documentation - Operators</a></li>
</ul>
<p><!-- FIM --></p>