TypeScript

Dominando NestJS Avançado: Guards, Interceptors, Pipes e Exception Filters em Projetos Reais

17 min de leitura

Dominando NestJS Avançado: Guards, Interceptors, Pipes e Exception Filters em Projetos Reais

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

<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 &#039;@nestjs/common&#039;;

import { JwtService } from &#039;@nestjs/jwt&#039;;

@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(&#039;Token não fornecido&#039;);

}

try {

const payload = this.jwtService.verify(token);

request.user = payload;

return true;

} catch (error) {

throw new UnauthorizedException(&#039;Token inválido ou expirado&#039;);

}

}

private extractTokenFromHeader(request: any): string | undefined {

const [type, token] = request.headers.authorization?.split(&#039; &#039;) ?? [];

return type === &#039;Bearer&#039; ? 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 &#039;@nestjs/common&#039;;

import { Reflector } from &#039;@nestjs/core&#039;;

export const ROLES_KEY = &#039;roles&#039;;

@Injectable()

export class RolesGuard implements CanActivate {

constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {

const requiredRoles = this.reflector.get&lt;string[]&gt;(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(&#039;Usuário não autenticado&#039;);

}

const hasRole = requiredRoles.some(role =&gt; user.roles?.includes(role));

if (!hasRole) {

throw new ForbiddenException(&#039;Você não tem permissão para acessar este recurso&#039;);

}

return true;

}

}</code></pre>

<p>Para usar este Guard, você cria um decorator customizado:</p>

<pre><code class="language-typescript">import { SetMetadata } from &#039;@nestjs/common&#039;;

export const Roles = (...roles: string[]) =&gt; SetMetadata(ROLES_KEY, roles);</code></pre>

<p>E então aplica no seu controller:</p>

<pre><code class="language-typescript">@Controller(&#039;admin&#039;)

export class AdminController {

@Post(&#039;users&#039;)

@UseGuards(JwtAuthGuard, RolesGuard)

@Roles(&#039;admin&#039;, &#039;moderator&#039;)

createUser(@Body() createUserDto: CreateUserDto) {

return { message: &#039;Usuário criado com sucesso&#039; };

}

}</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 &#039;@nestjs/common&#039;;

import { Observable } from &#039;rxjs&#039;;

import { tap } from &#039;rxjs/operators&#039;;

@Injectable()

export class LoggingInterceptor implements NestInterceptor {

private readonly logger = new Logger(LoggingInterceptor.name);

intercept(context: ExecutionContext, next: CallHandler): Observable&lt;any&gt; {

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) =&gt; {

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 &#039;@nestjs/common&#039;;

import { Observable } from &#039;rxjs&#039;;

import { map } from &#039;rxjs/operators&#039;;

export interface ResponseFormat&lt;T&gt; {

statusCode: number;

message: string;

data: T;

timestamp: string;

}

@Injectable()

export class TransformInterceptor&lt;T&gt; implements NestInterceptor&lt;T, ResponseFormat&lt;T&gt;&gt; {

intercept(context: ExecutionContext, next: CallHandler): Observable&lt;ResponseFormat&lt;T&gt;&gt; {

const response = context.switchToHttp().getResponse();

const statusCode = response.statusCode || 200;

return next.handle().pipe(

map((data) =&gt; ({

statusCode,

message: &#039;Sucesso&#039;,

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 &#039;@nestjs/core&#039;;

import { AppModule } from &#039;./app.module&#039;;

import { TransformInterceptor } from &#039;./interceptors/transform.interceptor&#039;;

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 &#039;class-validator&#039;;

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 &#039;@nestjs/common&#039;;

@Controller(&#039;users&#039;)

export class UsersController {

@Post()

@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))

createUser(@Body() createUserDto: CreateUserDto) {

return { message: &#039;Usuário criado&#039;, 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 &#039;@nestjs/common&#039;;

@Injectable()

export class ParsePositiveIntPipe implements PipeTransform&lt;string, number&gt; {

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 &lt;= 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(&#039;products&#039;)

export class ProductsController {

@Get(&#039;:id&#039;)

getProduct(@Param(&#039;id&#039;, ParsePositiveIntPipe) id: number) {

return { id, name: &#039;Produto Exemplo&#039; };

}

}</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 &#039;@nestjs/common&#039;;

import { Response } from &#039;express&#039;;

@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&lt;Response&gt;();

const request = ctx.getRequest();

const status = exception.getStatus();

const exceptionResponse = exception.getResponse();

const errorMessage = typeof exceptionResponse === &#039;object&#039;

? (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 &#039;@nestjs/common&#039;;

import { Response } from &#039;express&#039;;

@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&lt;Response&gt;();

const request = ctx.getRequest();

let status = HttpStatus.INTERNAL_SERVER_ERROR;

let message = &#039;Erro interno do servidor&#039;;

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 &#039;@nestjs/common&#039;;

export class InsufficientFundsException extends HttpException {

constructor(message: string = &#039;Saldo insuficiente&#039;) {

super(

{

statusCode: HttpStatus.PAYMENT_REQUIRED,

message,

code: &#039;INSUFFICIENT_FUNDS&#039;,

},

HttpStatus.PAYMENT_REQUIRED,

);

}

}</code></pre>

<p>E um filter específico para ela:</p>

<pre><code class="language-typescript">import { ExceptionFilter, Catch, ArgumentsHost } from &#039;@nestjs/common&#039;;

import { Response } from &#039;express&#039;;

import { InsufficientFundsException } from &#039;./exceptions/insufficient-funds.exception&#039;;

@Catch(InsufficientFundsException)

export class InsufficientFundsFilter implements ExceptionFilter {

catch(exception: InsufficientFundsException, host: ArgumentsHost) {

const ctx = host.switchToHttp();

const response = ctx.getResponse&lt;Response&gt;();

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 &#039;@nestjs/common&#039;;

import { JwtAuthGuard } from &#039;./guards/jwt-auth.guard&#039;;

import { RolesGuard, Roles } from &#039;./guards/roles.guard&#039;;

import { LoggingInterceptor } from &#039;./interceptors/logging.interceptor&#039;;

import { TransformInterceptor } from &#039;./interceptors/transform.interceptor&#039;;

import { CreateUserDto } from &#039;./dto/create-user.dto&#039;;

import { HttpExceptionFilter } from &#039;./filters/http-exception.filter&#039;;

import { UsersService } from &#039;./users.service&#039;;

@Controller(&#039;users&#039;)

@UseGuards(JwtAuthGuard, RolesGuard)

@UseInterceptors(LoggingInterceptor, TransformInterceptor)

@UseFilters(HttpExceptionFilter)

export class UsersController {

constructor(private usersService: UsersService) {}

@Post()

@Roles(&#039;admin&#039;)

@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 &#039;admin&#039;</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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Contract Testing com TypeScript: Pact e Verificação de APIs: Do Básico ao Avançado
Contract Testing com TypeScript: Pact e Verificação de APIs: Do Básico ao Avançado

O que é Contract Testing? Contract Testing é uma abordagem de testes que vali...

Guia Completo de Mapped Types em TypeScript: Transformando Tipos Existentes
Guia Completo de Mapped Types em TypeScript: Transformando Tipos Existentes

O Que São Mapped Types? Mapped Types são um recurso avançado do TypeScript qu...

Boas Práticas de Utility Types em TypeScript: Partial, Required, Pick, Omit e Outros para Times Ágeis
Boas Práticas de Utility Types em TypeScript: Partial, Required, Pick, Omit e Outros para Times Ágeis

Introdução aos Utility Types Os Utility Types são uma funcionalidade poderosa...