Introdução
O NestJS é hoje a framework de backend Node.js mais usada para construir aplicações server-side escaláveis, tipadas e fáceis de manter. Combina os conceitos mais sólidos da engenharia de software — Dependency Injection, módulos, arquitetura em camadas — com a produtividade do TypeScript e um ecossistema que cobre praticamente todas as necessidades: REST, GraphQL, WebSocket, microsserviços, CLI, cron jobs.
Neste guia construímos, passo a passo, uma REST API completa e pronta para produção: uma API de Blog com autenticação JWT, refresh token, controlo de acessos baseado em roles (RBAC), validação, upload de ficheiros, logging centralizado e gestão estruturada de erros — exatamente a stack por trás da maioria das APIs NestJS reais em 2026.
O que é o NestJS
O NestJS é uma framework opinionated construída sobre o Express (ou, alternativamente, o Fastify) que impõe uma estrutura precisa à aplicação: cada funcionalidade é organizada em módulos, cada módulo expõe controllers (tratam os pedidos HTTP) e providers (contêm a lógica de negócio, tipicamente services), ligados entre si através de um sistema de Dependency Injection integrado e inspirado no Angular.
Porque Usá-lo
- TypeScript nativo: tipagem end-to-end, autocompletar, refactoring seguro.
- Arquitetura imposta: ao contrário do Express puro, o NestJS obriga à separação de responsabilidades (controller/service/repository), reduzindo o "big ball of mud" típico de projetos Node que cresceram sem estrutura.
- Dependency Injection integrada: testabilidade elevada, baixo acoplamento, providers intercambiáveis (útil para mocks nos testes).
- Ecossistema maduro: módulos oficiais para TypeORM, Prisma, Mongoose, Passport, GraphQL, WebSocket, Bull/BullMQ, Swagger, gRPC, microsserviços.
- Decoradores declarativos: guards, interceptors, pipes e exception filters permitem cross-cutting concerns (auth, logging, validação) sem poluir a lógica de negócio.
Quando Compensa (e Quando Não)
O NestJS compensa quando o projeto tem uma complexidade que justifica uma estrutura rígida: equipas com vários programadores, APIs destinadas a crescer ao longo do tempo, necessidade de testabilidade elevada, aplicações enterprise com requisitos de segurança e compliance. É provavelmente excessivo para um script pontual ou um protótipo que será descartado numa semana — aí, o Express puro ou o Fastify "nu" continuam a ser mais rápidos de arrancar.
Vantagens e Desvantagens
| Vantagens | Desvantagens |
|---|---|
| Estrutura clara e consistente entre projetos diferentes | Curva de aprendizagem mais acentuada do que o Express puro (decoradores, DI, módulos) |
| Testabilidade nativa graças ao DI | Overhead de boilerplate em projetos muito pequenos |
| Ecossistema oficial amplo e bem mantido | Mais "magia" (decoradores, reflection) em comparação com código explícito |
| Ótima integração com TypeScript e Swagger/OpenAPI | Bundle size e cold start ligeiramente superiores a frameworks minimalistas (relevante em ambientes serverless) |
| Facilita a adoção de Clean Architecture / hexagonal | Requer disciplina da equipa para não abusar da flexibilidade dos módulos |
Casos Reais
O NestJS é usado em produção por empresas como Adidas, Roche, Autodesk e Decathlon, além de ser uma escolha muito comum para backends SaaS B2B, plataformas de e-commerce, sistemas de gestão de utilizadores e APIs de microsserviços que precisam de comunicar entre si via gRPC ou filas de mensagens (RabbitMQ, Kafka).
Pré-requisitos
Antes de começar, certifica-te de que tens estas ferramentas instaladas e conheces o básico de TypeScript (interfaces, decoradores, generics) e conceitos REST (verbos HTTP, status codes, idempotência).
| Ferramenta | Versão recomendada | Para que serve |
|---|---|---|
| Node.js | 20.x LTS ou superior | Runtime JavaScript sobre o qual o NestJS corre |
| npm | 10.x (incluído no Node 20) | Gestão de pacotes |
| NestJS CLI | @nestjs/cli 10.x+ | Scaffolding de módulos, controllers, services |
| TypeScript | 5.x | Linguagem em que o NestJS e a tua app estão escritos |
| VS Code | Última versão estável | Editor com suporte nativo para TypeScript |
| PostgreSQL | 15.x ou superior | Base de dados relacional para o exemplo com TypeORM |
| Docker (opcional) | 24.x+ | Executar o PostgreSQL localmente sem instalação nativa |
# Verifica as versões instaladas
node -v
npm -v
# Instala a CLI do NestJS globalmente
npm install -g @nestjs/cli
nest --version
Sugestão: se não quiseres instalar o PostgreSQL nativamente, arranca-o com Docker:
docker run --name pg-blog -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=blog_db -p 5432:5432 -d postgres:16.
Arquitetura
Antes de escrever código, é fundamental perceber como o NestJS encaminha um pedido HTTP através dos seus componentes principais. Eis o ciclo de vida completo de um pedido:
Client
│
▼
┌─────────────┐
│ Middleware │ (ex. logger, cookie-parser — executado antes do routing)
└──────┬───────┘
▼
┌─────────────┐
│ Guard │ (ex. AuthGuard — decide se o pedido pode prosseguir)
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ (fase "antes" — ex. logging, transformação do input)
└──────┬───────┘
▼
┌─────────────┐
│ Pipe │ (valida e transforma os parâmetros de entrada)
└──────┬───────┘
▼
┌─────────────┐
│ Controller │ (recebe o pedido, delega ao service)
└──────┬───────┘
▼
┌─────────────┐
│ Service │ (lógica de negócio, chama os repositories)
└──────┬───────┘
▼
┌─────────────┐
│ Repository / │ (acesso aos dados — TypeORM, Prisma, Mongoose...)
│ Database │
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ (fase "depois" — ex. transformação da resposta)
└──────┬───────┘
▼
┌──────────────────┐
│ Exception Filter │ (intercepta APENAS se for lançada uma exceção)
└──────┬────────────┘
▼
Response
Controller
A camada mais externa: recebe os pedidos HTTP, extrai parâmetros/body/query e delega imediatamente a lógica ao service correspondente. Um controller nunca deve conter lógica de negócio — a sua única responsabilidade é o mapeamento HTTP ↔ chamada de método.
Service
Contém a lógica de negócio propriamente dita. É um provider injetável,
tipicamente marcado com @Injectable(), e é injetado no controller (ou noutros
services) através do construtor.
Module
O módulo é a unidade organizacional do NestJS: agrupa controllers, providers e imports de
outros módulos. Toda a aplicação tem pelo menos um AppModule raiz, e as
funcionalidades são tipicamente isoladas em feature modules (ex.
PostsModule, AuthModule, UsersModule).
Provider
Qualquer classe gerida pelo contentor de Dependency Injection do Nest: services,
repositories, factories, helpers. É declarado em providers no módulo e pode ser
injetado onde estiver disponível (no mesmo módulo, ou exportado para outros módulos).
Middleware
Funções executadas antes do routing do Nest, com acesso direto a
req, res e next() — o mesmo modelo do Express. Útil
para logging bruto, parsing de cookies, ou headers customizados aplicados globalmente.
Guard
Decidem se um pedido pode prosseguir, devolvendo true/
false (ou lançando uma exceção). São o local correto para autenticação e
autorização — nunca dentro do controller ou do service.
Interceptor
Posicionam-se em torno da execução do handler da rota (como middleware AOP): podem transformar o pedido antes de chegar ao controller e a resposta antes de sair. Casos de uso típicos: logging dos tempos de resposta, transformação uniforme das respostas, caching, gestão de timeouts.
Pipe
Transformam e validam os dados de entrada (parâmetros de rota, query string, body) antes de
chegarem ao controller. O ValidationPipe, integrado com
class-validator, é o pipe mais usado em qualquer API NestJS séria.
Exception Filter
Interceptam exceções lançadas em qualquer ponto do pedido e transformam-nas numa resposta HTTP consistente (status code, corpo JSON estruturado), evitando que stack traces ou erros em bruto cheguem ao cliente.
Instalação
# 1. Cria um novo projeto NestJS
nest new blog-api
# Durante a criação, escolhe npm como package manager quando solicitado
cd blog-api
# 2. Instala as dependências para a base de dados (TypeORM + driver PostgreSQL)
npm install @nestjs/typeorm typeorm pg
# 3. Instala as dependências para a validação
npm install class-validator class-transformer
# 4. Instala as dependências para autenticação JWT
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install --save-dev @types/passport-jwt @types/bcrypt
# 5. Instala as dependências para segurança e rate limiting
npm install helmet @nestjs/throttler
# 6. Instala as dependências para upload de ficheiros
npm install @nestjs/platform-express multer
npm install --save-dev @types/multer
# 7. Instala o Swagger para documentação automática da API
npm install @nestjs/swagger
# 8. Instala o logger estruturado
npm install nestjs-pino pino-http pino-pretty
# 9. Configura as variáveis de ambiente
npm install @nestjs/config
Cada comando instala um bloco funcional bem preciso: @nestjs/typeorm +
typeorm + pg ligam o Nest ao PostgreSQL através do ORM TypeORM;
class-validator/class-transformer ativam DTOs validados
automaticamente; a stack passport/passport-jwt/bcrypt
constrói todo o fluxo de autenticação; helmet e
@nestjs/throttler reforçam a segurança HTTP e o rate limiting;
multer trata o multipart/form-data para uploads; nestjs-pino
fornece logging estruturado em JSON, adequado para produção.
Implementação Passo a Passo
1. Estrutura de Pastas
src/
├── main.ts
├── app.module.ts
├── config/
│ └── configuration.ts
├── common/
│ ├── filters/
│ │ └── http-exception.filter.ts
│ ├── interceptors/
│ │ ├── logging.interceptor.ts
│ │ └── transform.interceptor.ts
│ ├── guards/
│ │ └── roles.guard.ts
│ ├── decorators/
│ │ └── roles.decorator.ts
│ └── pipes/
│ └── parse-object-id.pipe.ts
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── strategies/
│ │ ├── jwt.strategy.ts
│ │ └── jwt-refresh.strategy.ts
│ └── dto/
│ ├── login.dto.ts
│ └── register.dto.ts
├── users/
│ ├── users.module.ts
│ ├── users.service.ts
│ └── entities/
│ └── user.entity.ts
└── posts/
├── posts.module.ts
├── posts.controller.ts
├── posts.service.ts
├── entities/
│ └── post.entity.ts
└── dto/
├── create-post.dto.ts
└── update-post.dto.ts
2. Configuração Centralizada com @nestjs/config
// src/config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT ?? '3000', 10),
database: {
host: process.env.DB_HOST ?? 'localhost',
port: parseInt(process.env.DB_PORT ?? '5432', 10),
username: process.env.DB_USERNAME ?? 'postgres',
password: process.env.DB_PASSWORD ?? 'postgres',
name: process.env.DB_NAME ?? 'blog_db',
},
jwt: {
accessSecret: process.env.JWT_ACCESS_SECRET ?? 'change-me-access',
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? '15m',
refreshSecret: process.env.JWT_REFRESH_SECRET ?? 'change-me-refresh',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
},
});
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import configuration from './config/configuration';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
type: 'postgres',
host: cfg.get('database.host'),
port: cfg.get('database.port'),
username: cfg.get('database.username'),
password: cfg.get('database.password'),
database: cfg.get('database.name'),
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production', // ⚠️ apenas em desenvolvimento
}),
}),
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
AuthModule,
UsersModule,
PostsModule,
],
})
export class AppModule {}
Linha a linha: ConfigModule.forRoot({ isGlobal: true }) torna o
ConfigService disponível em toda a aplicação sem ter de reimportar o módulo em
todo o lado. O TypeOrmModule.forRootAsync constrói a ligação à base de dados de
forma assíncrona, lendo os valores do ConfigService em vez de um objeto estático
— necessário para poder usar variáveis de ambiente. O synchronize: true faz com
que o TypeORM crie/atualize automaticamente as tabelas com base nas entities: muito prático em
desenvolvimento, perigoso em produção (pode apagar dados), onde se usarão
migrations.
3. Entities com TypeORM
// src/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm';
import { Post } from '../../posts/entities/post.entity';
import { Exclude } from 'class-transformer';
export enum UserRole {
ADMIN = 'admin',
AUTHOR = 'author',
READER = 'reader',
}
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
@Exclude() // exclui o hash da password das respostas serializadas
passwordHash: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.READER })
role: UserRole;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
}
// src/posts/entities/post.entity.ts
import {
Entity, Column, PrimaryGeneratedColumn, ManyToOne,
CreateDateColumn, UpdateDateColumn, Index,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Index({ unique: true })
@Column()
slug: string;
@Column('text')
content: string;
@Column({ default: false })
published: boolean;
@Column({ nullable: true })
coverImageUrl?: string;
@ManyToOne(() => User, (user) => user.posts, { eager: true })
author: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
4. DTOs com class-validator
// src/posts/dto/create-post.dto.ts
import { IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreatePostDto {
@IsString()
@MinLength(5)
@MaxLength(200)
title: string;
@IsString()
@MinLength(20)
content: string;
@IsBoolean()
@IsOptional()
published?: boolean;
}
// src/posts/dto/update-post.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';
export class UpdatePostDto extends PartialType(CreatePostDto) {}
O PartialType gera automaticamente uma versão em que todos os campos do DTO
original se tornam opcionais — perfeito para updates parciais (PATCH), sem
duplicar decoradores de validação.
5. Ativar o ValidationPipe Globalmente
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
app.enableCors({ origin: process.env.CORS_ORIGIN?.split(',') ?? '*' });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // remove propriedades não presentes no DTO
forbidNonWhitelisted: true, // lança erro se chegarem propriedades extra
transform: true, // converte automaticamente os tipos (ex. string → number)
}),
);
app.useGlobalFilters(new HttpExceptionFilter());
app.setGlobalPrefix('api/v1');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
whitelist: true + forbidNonWhitelisted: true são o par mais
importante para a segurança dos DTOs: sem isso, um cliente poderia enviar campos extra (ex.
role: 'admin' num pedido de registo) que o TypeORM poderia inadvertidamente
persistir se o código não os filtrar explicitamente noutro lugar.
6. Middleware Personalizado
// src/common/middleware/request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
req['requestId'] = randomUUID();
res.setHeader('X-Request-Id', req['requestId']);
next();
}
}
// registo em app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}
7. Guards: JwtAuthGuard e RolesGuard
// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('jwt.accessSecret'),
});
}
async validate(payload: { sub: string; email: string; role: string }) {
// O valor devolvido é anexado a req.user
return { userId: payload.sub, email: payload.email, role: payload.role };
}
}
// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../../users/entities/user.entity';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
// src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { UserRole } from '../../users/entities/user.entity';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true; // sem restrição de role nesta rota
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user?.role);
}
}
O padrão Reflector.getAllAndOverride lê os metadados definidos pelo decorador
@Roles(...) tanto a nível de método individual como de classe inteira, permitindo
definir uma restrição de role num controller inteiro e substituí-la em rotas individuais
quando necessário.
8. Interceptors: Logging e Transformação da Resposta
// src/common/interceptors/logging.interceptor.ts
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('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable {
const req = context.switchToHttp().getRequest();
const start = Date.now();
return next.handle().pipe(
tap(() => {
const ms = Date.now() - start;
this.logger.log(`${req.method} ${req.url} — ${ms}ms`);
}),
);
}
}
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response {
success: true;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor implements NestInterceptor> {
intercept(context: ExecutionContext, next: CallHandler): Observable> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
9. Exception Filter Global
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.getResponse()
: 'Errore interno del server';
// Regista o stack trace completo apenas no servidor, nunca na resposta ao cliente
if (!(exception instanceof HttpException)) {
this.logger.error(exception instanceof Error ? exception.stack : exception);
}
response.status(status).json({
success: false,
statusCode: status,
path: request.url,
timestamp: new Date().toISOString(),
message,
});
}
}
Um detalhe de segurança crucial: quando a exceção não é uma
HttpException conhecida (ou seja, um erro inesperado, ex. um bug ou um erro da
base de dados), a mensagem devolvida ao cliente é genérica ("Erro interno do servidor") — o
stack trace real é apenas registado no servidor. Expor stack traces aos clientes é um problema
de segurança conhecido (information disclosure).
10. Pipe Personalizado
// src/common/pipes/parse-uuid-or-404.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, NotFoundException } from '@nestjs/common';
import { isUUID } from 'class-validator';
@Injectable()
export class ParseUuidOr404Pipe implements PipeTransform {
transform(value: string, _metadata: ArgumentMetadata): string {
if (!isUUID(value)) {
throw new NotFoundException(`Risorsa con id "${value}" non trovata`);
}
return value;
}
}
Devolver um 404 em vez de um genérico 400 Bad Request quando o id
nem sequer é um UUID válido é uma escolha deliberada: do ponto de vista do cliente, "recurso
não encontrado" é semanticamente mais correto e não revela detalhes sobre o formato interno
dos ids.
Exemplo Real: API de Blog Completa
Vamos juntar todas as peças num módulo PostsModule completo, com CRUD,
autenticação, autorização baseada em roles e upload da imagem de capa.
Auth Service: login, register, refresh token
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private config: ConfigService,
) {}
async register(dto: RegisterDto) {
const existing = await this.usersService.findByEmail(dto.email);
if (existing) throw new ConflictException('Email già registrata');
const passwordHash = await bcrypt.hash(dto.password, 12);
const user = await this.usersService.create({ ...dto, passwordHash });
return this.buildTokens(user.id, user.email, user.role);
}
async login(dto: LoginDto) {
const user = await this.usersService.findByEmail(dto.email);
if (!user) throw new UnauthorizedException('Credenziali non valide');
const passwordValid = await bcrypt.compare(dto.password, user.passwordHash);
if (!passwordValid) throw new UnauthorizedException('Credenziali non valide');
return this.buildTokens(user.id, user.email, user.role);
}
async refresh(userId: string, email: string, role: string) {
// Em produção: verifica também se o refresh token ainda é válido no servidor
// (whitelist/blacklist) para poder revogá-lo, ex. no logout.
return this.buildTokens(userId, email, role);
}
private buildTokens(sub: string, email: string, role: string) {
const payload = { sub, email, role };
const accessToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.accessSecret'),
expiresIn: this.config.get('jwt.accessExpiresIn'),
});
const refreshToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.refreshSecret'),
expiresIn: this.config.get('jwt.refreshExpiresIn'),
});
return { accessToken, refreshToken };
}
}
Porque dois tokens separados? O access token tem uma vida curta (15 minutos) e é o que é enviado em cada pedido — se for roubado, o dano é limitado no tempo. O refresh token tem uma vida mais longa (dias) mas só é usado para obter um novo access token num endpoint dedicado, reduzindo a superfície de ataque.
Auth Controller
// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@UseGuards(AuthGuard('jwt-refresh'))
@Post('refresh')
refresh(@Req() req) {
return this.authService.refresh(req.user.userId, req.user.email, req.user.role);
}
}
Posts Controller: CRUD, RBAC e Upload
// src/posts/posts.controller.ts
import {
Controller, Get, Post, Body, Patch, Param, Delete,
UseGuards, UseInterceptors, UploadedFile, Query,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { AuthGuard } from '@nestjs/passport';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { UserRole } from '../users/entities/user.entity';
import { ParseUuidOr404Pipe } from '../common/pipes/parse-uuid-or-404.pipe';
import { CurrentUser } from '../common/decorators/current-user.decorator';
@Controller('posts')
export class PostsController {
constructor(private postsService: PostsService) {}
@Get()
findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.postsService.findAll(Number(page), Number(limit));
}
@Get(':id')
findOne(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.findOne(id);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Post()
create(@Body() dto: CreatePostDto, @CurrentUser() user) {
return this.postsService.create(dto, user.userId);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Patch(':id')
update(@Param('id', ParseUuidOr404Pipe) id: string, @Body() dto: UpdatePostDto, @CurrentUser() user) {
return this.postsService.update(id, dto, user.userId, user.role);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.ADMIN)
@Delete(':id')
remove(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.remove(id);
}
@UseGuards(AuthGuard('jwt'))
@Post(':id/cover')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (_req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
cb(null, allowed.includes(file.mimetype));
},
}))
uploadCover(@Param('id', ParseUuidOr404Pipe) id: string, @UploadedFile() file: Express.Multer.File) {
return this.postsService.setCoverImage(id, file);
}
}
Repara como cada rota aplica apenas os guards e as roles estritamente
necessárias: findAll/findOne são públicas (sem guard),
create/update exigem a role author ou
admin, enquanto remove é reservada apenas ao admin —
um exemplo concreto do princípio do menor privilégio aplicado rota a rota.
Posts Service
// src/posts/posts.service.ts
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './entities/post.entity';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { UserRole } from '../users/entities/user.entity';
import slugify from 'slugify';
@Injectable()
export class PostsService {
constructor(@InjectRepository(Post) private postsRepo: Repository) {}
async findAll(page: number, limit: number) {
const [items, total] = await this.postsRepo.findAndCount({
where: { published: true },
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return { items, total, page, limit };
}
async findOne(id: string): Promise {
const post = await this.postsRepo.findOne({ where: { id } });
if (!post) throw new NotFoundException(`Post ${id} non trovato`);
return post;
}
async create(dto: CreatePostDto, authorId: string): Promise {
const post = this.postsRepo.create({
...dto,
slug: slugify(dto.title, { lower: true, strict: true }),
author: { id: authorId } as any,
});
return this.postsRepo.save(post);
}
async update(id: string, dto: UpdatePostDto, userId: string, userRole: UserRole): Promise {
const post = await this.findOne(id);
// Um author só pode editar os seus próprios posts; o admin pode editar todos
if (userRole !== UserRole.ADMIN && post.author.id !== userId) {
throw new ForbiddenException('Non puoi modificare un post di un altro autore');
}
Object.assign(post, dto);
return this.postsRepo.save(post);
}
async remove(id: string): Promise {
const result = await this.postsRepo.delete(id);
if (result.affected === 0) throw new NotFoundException(`Post ${id} non trovato`);
}
async setCoverImage(id: string, file: Express.Multer.File): Promise {
const post = await this.findOne(id);
// Em produção: faz upload para S3/Cloud Storage em vez de disco local
post.coverImageUrl = `/uploads/${file.filename}`;
return this.postsRepo.save(post);
}
}
Repara na verificação userRole !== UserRole.ADMIN && post.author.id !== userId
no método update: é um exemplo de autorização ao nível do recurso
(não apenas da rota) — um author autenticado com um token JWT válido pode ainda assim ser
bloqueado se estiver a tentar modificar o recurso de outra pessoa. Esta verificação deve
sempre ser feita no service, nunca delegada apenas ao guard, que opera a um nível demasiado
genérico para conhecer o proprietário de um recurso específico.
Boas Práticas
| Princípio | Aplicação Prática no NestJS |
|---|---|
| Clean Architecture | Separação clara entre controller (HTTP), service (lógica de negócio) e repository (persistência). O domínio nunca deve depender de detalhes de infraestrutura como Express ou TypeORM. |
| SOLID — Single Responsibility | Um controller mapeia pedidos HTTP, um service contém uma única área de lógica de negócio, um repository um único aggregate. |
| SOLID — Dependency Inversion | Os services dependem de interfaces/tokens abstratos (ex. @InjectRepository), não de implementações concretas — substituíveis por mocks nos testes. |
| DRY | Lógica de validação partilhada nos DTOs com PartialType/PickType/OmitType, evitando duplicação entre DTO de Create e Update. |
| KISS | Evita introduzir CQRS, Event Sourcing ou microsserviços até que a complexidade real do domínio o justifique. |
| Repository Pattern | O TypeORM/Prisma já fornecem esta camada; evita "furar" a abstração chamando queries SQL em bruto diretamente nos services, exceto em casos críticos de performance. |
| DTO | Cada endpoint tem um DTO de entrada dedicado — nunca expor diretamente as entities da base de dados como contrato de API. |
| Validation | ValidationPipe global com whitelist e forbidNonWhitelisted sempre ativos em todos os projetos, sem exceções. |
Performance
Caching
// Caching ao nível do endpoint com @nestjs/cache-manager
import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
import { UseInterceptors } from '@nestjs/common';
@UseInterceptors(CacheInterceptor)
@CacheTTL(60) // cache por 60 segundos
@Get()
findAll() {
return this.postsService.findAll(1, 10);
}
Lazy Loading de Módulos
Em aplicações monolíticas de grande dimensão, o LazyModuleLoader permite
carregar módulos pouco usados (ex. um módulo de relatórios) apenas quando efetivamente
necessários, reduzindo o tempo de bootstrap.
Queries Otimizadas
- Usa
selectexplícito no TypeORM para evitar transferir colunas desnecessárias (ex. excluircontentnas listas). - Adiciona índices às colunas usadas frequentemente em
WHERE/ORDER BY(vê o@Indexno slug na entityPost). - Evita o problema N+1: usa
relationsouQueryBuildercomleftJoinAndSelectem vez de carregar relações dentro de um loop. - Usa sempre paginação (
skip/take) — nunca devolvas coleções ilimitadas.
Logging e Monitorização
Em produção, substitui o logger predefinido por nestjs-pino para logs JSON
estruturados, facilmente indexáveis por ferramentas como Grafana Loki ou Datadog. Para a
monitorização de performance, integra o @nestjs/terminus para health checks
(/health) usados por load balancers e orquestradores (Kubernetes, Railway).
Profiling
Para identificar gargalos reais, usa node --prof ou ferramentas APM (Application
Performance Monitoring) como New Relic ou Elastic APM, que rastreiam o tempo gasto em cada
handler, query e chamada externa — evita otimizar "por sensação" sem dados reais.
Segurança
| Medida | Como Implementar |
|---|---|
| JWT | Access token de vida curta assinado com um secret robusto (32+ caracteres aleatórios), nunca hardcoded no código. |
| Refresh Token | Vida mais longa, idealmente com possibilidade de revogação (whitelist em Redis/DB), rotação em cada uso. |
| Hash de Password / bcrypt | bcrypt.hash(password, 12) — nunca MD5/SHA1, nunca passwords em texto simples na BD. |
| Helmet | app.use(helmet()) define headers de segurança (CSP, X-Frame-Options, HSTS) numa única linha. |
| CORS | app.enableCors({ origin: [...] }) com whitelist explícita de domínios, nunca origin: '*' em produção se a API exigir credentials. |
| Rate Limiting | @nestjs/throttler para limitar pedidos por IP, essencial em endpoints como /auth/login para mitigar ataques de força bruta. |
| ValidationPipe | whitelist + forbidNonWhitelisted para bloquear mass assignment em campos não previstos. |
| Sanitização de Input | class-validator para a estrutura, mais sanitização explícita de HTML (ex. sanitize-html) se o campo permitir markup livre, para prevenir XSS. |
// Exemplo: rate limiting mais agressivo especificamente no login
import { Throttle } from '@nestjs/throttler';
@Throttle({ default: { limit: 5, ttl: 60000 } }) // máx. 5 tentativas por minuto por IP
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
Erros Comuns
1. Esquecer whitelist/forbidNonWhitelisted no ValidationPipe
Problema: um cliente pode enviar campos extra não previstos pelo DTO (ex. role: 'admin').
Causa: ValidationPipe configurado sem whitelist: true.
Solução: ativa sempre whitelist e forbidNonWhitelisted globalmente em main.ts.
2. synchronize: true em Produção
Problema: o TypeORM pode alterar/apagar colunas ou tabelas em runtime.
Causa: confusão entre o ambiente de desenvolvimento e produção na configuração da base de dados.
Solução: synchronize: false em produção, gere o schema com migrations explícitas (typeorm migration:generate).
3. Dependência Circular Entre Módulos
Problema: erro "Nest cannot resolve dependencies" ou crash silencioso no arranque.
Causa: dois módulos importam-se mutuamente de forma direta.
Solução: usa forwardRef(() => ModuleB) em ambos os módulos, ou — melhor ainda — extrai a dependência partilhada para um terceiro módulo.
4. Expor as Entities Diretamente como Resposta da API
Problema: campos sensíveis (ex. passwordHash) acabam na resposta JSON.
Causa: o controller devolve a entity TypeORM tal como está, sem uma camada DTO/serializer.
Solução: usa o @Exclude() do class-transformer na entity juntamente com um ClassSerializerInterceptor global, ou melhor ainda um DTO de resposta explícito.
5. Passwords Guardadas Sem Hashing
Problema: um comprometimento da BD expõe todas as passwords em texto simples.
Causa: omissão (frequentemente durante o rapid prototyping) do passo de hashing.
Solução: hash com bcrypt/argon2 sempre, mesmo em desenvolvimento — nunca há uma razão válida para o saltar.
6. Secrets JWT Fracos ou Hardcoded no Código
Problema: um secret previsível permite falsificar tokens válidos.
Causa: um secret predefinido deixado em produção, ou commitado no repositório.
Solução: secrets gerados aleatoriamente (pelo menos 256 bits), geridos apenas através de variáveis de ambiente/secret manager, nunca no código fonte.
7. Falta de Gestão de Erros Assíncronos nos Services
Problema: promise rejections não tratadas provocam crash do processo Node.
Causa: chamadas async sem try/catch onde necessário, ou esquecimento do exception filter global.
Solução: deixa que as exceções subam até ao exception filter global (comportamento predefinido do Nest), intercepta apenas onde é necessário um comportamento diferente (fallback, retry).
8. Queries N+1 Não Detetadas
Problema: o endpoint torna-se extremamente lento com o crescimento dos dados.
Causa: carregamento de relações dentro de um loop em vez de um único join.
Solução: usa relations/leftJoinAndSelect, monitoriza as queries geradas com logging: true em desenvolvimento.
9. CORS Aberto a Todos em Produção com Credentials
Problema: qualquer website pode fazer pedidos autenticados à API em nome do utilizador.
Causa: origin: '*' combinado com credentials: true.
Solução: whitelist explícita dos domínios autorizados.
10. Sem Rate Limiting nos Endpoints de Autenticação
Problema: ataques de força bruta ao login/registo.
Causa: @nestjs/throttler não aplicado, ou com limites demasiado permissivos.
Solução: um @Throttle mais restritivo especificamente no login/registo/reset de password.
11. DTO de Update Idêntico ao DTO de Create Sem PartialType
Problema: cada PATCH exige todos os campos obrigatórios, quebrando os updates parciais.
Causa: o DTO de Create é copiado sem tornar os campos opcionais.
Solução: class UpdateXDto extends PartialType(CreateXDto) {}.
12. Guards Aplicados Apenas ao Nível da Rota, Nunca do Recurso
Problema: um utilizador autenticado pode modificar recursos de outros utilizadores.
Causa: confia-se apenas no @UseGuards(AuthGuard('jwt')) sem verificação de ownership no service.
Solução: verificação explícita resource.ownerId === user.id (ou role admin) no service, como mostrado no exemplo PostsService.update.
13. Ligação à Base de Dados Sem Pool Configurado Corretamente
Problema: esgotamento de ligações sob carga, erros "too many clients".
Causa: valores predefinidos do pool não adequados ao tráfego real.
Solução: configura explicitamente extra: { max: 20 } na connection do TypeORM, calibrado com o plano da base de dados.
14. Upload de Ficheiros Sem Limites de Tamanho ou Tipo
Problema: um utilizador malicioso carrega ficheiros enormes ou executáveis disfarçados de imagens.
Causa: FileInterceptor configurado sem limits nem fileFilter.
Solução: define sempre limits.fileSize e valida o mimetype, como no exemplo uploadCover.
15. Variáveis de Ambiente Não Validadas no Arranque
Problema: a app arranca "silenciosamente" com configuração incompleta e falha de forma críptica mais tarde.
Causa: nenhum schema de validação sobre as env vars.
Solução: usa ConfigModule.forRoot({ validationSchema: Joi.object({...}) }) para falhar o bootstrap imediatamente se faltar uma variável crítica.
16. Logging de Dados Sensíveis
Problema: passwords, tokens ou dados pessoais acabam nos logs em texto simples.
Causa: logging indiscriminado de todo o body do pedido.
Solução: redige (mascara) explicitamente os campos sensíveis antes do logging, ou usa os redact paths do pino.
17. Testes Escritos Apenas para o "Happy Path"
Problema: bugs que só surgem com input inválido ou edge cases permanecem invisíveis até à produção.
Causa: pressão de tempo, cobertura de testes limitada ao fluxo principal.
Solução: testa explicitamente os erros 4xx esperados (validação, permissões, not found), não apenas os 2xx.
18. Ausência de Versionamento da API
Problema: cada alteração breaking quebra os clientes existentes sem aviso.
Causa: nenhum prefixo de versão (/api/v1) ou estratégia de versionamento do Nest configurada.
Solução: app.setGlobalPrefix('api/v1') desde o primeiro dia, mesmo em projetos pequenos.
19. Dependências Desatualizadas com Vulnerabilidades Conhecidas
Problema: a API permanece exposta a CVEs publicamente documentadas.
Causa: falta de um processo de auditoria periódica das dependências.
Solução: npm audit integrado na CI, atualizações regulares com Dependabot/Renovate.
20. Nenhuma Distinção Entre Erros de Validação e Erros de Domínio
Problema: todos os erros voltam como 500 genérico, tornando impossível para o cliente distinguir um input errado de um problema do servidor.
Causa: uso de Error genérico em vez das classes HttpException específicas do Nest.
Solução: usa sempre as exceções tipadas corretas (BadRequestException, NotFoundException, ForbiddenException, ConflictException...) — é assim que o Nest comunica o status code HTTP correto.
FAQ
1. O NestJS é adequado também para projetos pequenos?
Sim, mas o verdadeiro valor surge quando o projeto cresce: num script pequeno, a estrutura pode parecer excessiva em comparação com o Express puro.
2. Tenho de usar TypeScript obrigatoriamente?
Tecnicamente o NestJS também suporta JavaScript puro, mas perde-se grande parte do valor (decoradores tipados, DI type-safe). É fortemente desaconselhado em produção.
3. TypeORM ou Prisma?
O TypeORM integra-se de forma mais nativa com os decoradores do NestJS e é mais maduro no ecossistema Nest; o Prisma oferece um client gerado mais type-safe e uma melhor developer experience nas migrations, mas requer uma camada de integração manual com o Nest.
4. Como faço a gestão das migrations da base de dados?
Com o TypeORM: typeorm migration:generate para as gerar a partir do diff das entities, typeorm migration:run para as aplicar em CI/CD, nunca synchronize: true em produção.
5. Como estruturo um projeto muito grande?
Modulariza por domínio (feature modules), não por tipo técnico — evita pastas globais "controllers/", "services/" que misturam domínios diferentes.
6. Como faço testing dos controllers?
Com Test.createTestingModule do @nestjs/testing, mockando os services injetados; para os services, mocka os repositories TypeORM com getRepositoryToken.
7. Qual é a diferença entre Guard e Middleware para autenticação?
O middleware não tem acesso ao contexto de execução do Nest (ex. metadados dos decoradores), logo não consegue ler @Roles(). Os guards conseguem — por isso a autenticação/autorização vai sempre nos guards, nunca nos middlewares.
8. Posso usar GraphQL em vez de REST?
Sim, o NestJS tem suporte oficial tanto para Apollo Server como para Mercurius, com o mesmo sistema de módulos/DI — útil se precisares de queries flexíveis do lado do cliente.
9. Como faço a gestão de transações na base de dados?
Com DataSource.transaction() do TypeORM, ou com o decorador @Transactional() da biblioteca typeorm-transactional para uma sintaxe mais declarativa.
10. Como implemento o refresh token de forma segura?
Guarda um hash do refresh token no servidor (whitelist), roda-o a cada uso (token rotation), e invalida-o explicitamente no logout.
11. O NestJS suporta microsserviços?
Sim, nativamente, com transport layers para TCP, Redis, RabbitMQ, Kafka, gRPC e NATS através do @nestjs/microservices.
12. Como faço deploy para produção?
Build com nest build (compila para dist/), depois node dist/main.js; em plataformas como Railway/Render/Fly.io basta um Dockerfile multi-stage ou o buildpack nativo do Node.
13. Como faço a gestão de variáveis de ambiente diferentes para dev/staging/prod?
ConfigModule.forRoot({ envFilePath: ['.env.${NODE_ENV}', '.env'] }), com os valores reais injetados pela plataforma de hosting em produção, não a partir de ficheiros .env commitados.
14. Como implemento upload para S3 em vez de disco local?
Substitui o storage engine do Multer por multer-s3, ou faz o upload manualmente no service com o SDK da AWS depois de receber o buffer em memória.
15. Como documento automaticamente a API?
Com o @nestjs/swagger: decoradores @ApiTags, @ApiProperty nos DTOs, e SwaggerModule.setup() em main.ts geram uma UI OpenAPI interativa em /api/docs.
16. Como implemento soft delete?
O TypeORM suporta nativamente @DeleteDateColumn() na entity: softDelete() define a coluna em vez de remover a linha, e as queries excluem automaticamente os registos eliminados.
17. Como faço a gestão de múltiplas versões da API em simultâneo?
Com app.enableVersioning({ type: VersioningType.URI }) e o decorador @Version('2') nos controllers/métodos individuais que precisam de coexistir com a v1.
18. É necessário um ORM ou posso usar queries SQL diretas?
Para a maioria dos casos um ORM reduz erros e boilerplate; para queries muito complexas ou críticas em performance, o TypeORM permite mesmo assim queries raw através do QueryRunner, mantendo o resto da app no ORM.
19. Como implemento WebSocket no NestJS?
Com @nestjs/websockets e um @WebSocketGateway(), que se integra com Socket.IO ou ws mantendo o mesmo sistema de DI e guards das rotas REST.
20. Como faço a gestão de cron jobs?
Com @nestjs/schedule e o decorador @Cron('0 0 * * *') num método de um service — útil para limpeza de dados, envio de digests por email, sincronizações periódicas.
21. Como protejo a API contra ataques de SQL Injection?
O TypeORM/Prisma parametrizam automaticamente as queries — o risco só surge se construíres queries raw concatenando strings manualmente, algo que deve ser sempre evitado.
22. Qual é a forma correta de gerir secrets em produção?
Nunca no código ou em ficheiros commitados: usa as variáveis de ambiente da plataforma de hosting ou um secret manager dedicado (AWS Secrets Manager, Doppler, Railway Variables).
23. Como escalo horizontalmente uma API NestJS?
A app deve ser stateless (sem sessões locais em memória — usa JWT ou Redis para o estado partilhado), depois é replicada atrás de um load balancer; para WebSocket é necessário um adapter Redis para sincronizar as ligações entre instâncias.
24. Como faço a gestão da retrocompatibilidade quando altero um schema de resposta?
Introduz um novo campo em vez de renomear o existente, deprecia-o gradualmente com documentação clara, e usa o versionamento da API para alterações realmente breaking.
25. Vale a pena escrever testes E2E além dos unit tests para uma API NestJS?
Sim — o @nestjs/testing com supertest permite testar toda a pipeline (incluindo guards, pipes, interceptors) chamando os endpoints reais numa instância da app em memória, apanhando problemas de integração que os unit tests sozinhos não veem.
Recursos e Documentação Oficial
Para aprofundar cada tema abordado neste guia, a documentação oficial continua a ser a fonte mais fiável e atualizada:
- NestJS — documentação oficial:
docs.nestjs.com, em particular as secções sobre Guards, Interceptors, Pipes e Exception Filters. - TypeORM — documentação oficial:
typeorm.io, para relações, migrations e o query builder avançado. - Passport.js:
passportjs.org, a biblioteca de autenticação em que o@nestjs/passportse baseia. - class-validator: repositório oficial no GitHub, com a lista completa dos decoradores de validação disponíveis para além dos usados neste guia.
- OWASP API Security Top 10: a checklist de referência para a segurança de APIs REST, complementar à secção de Segurança deste guia.
- jwt.io: ferramenta para inspecionar e descodificar tokens JWT durante o debug da autenticação.
Artigos Relacionados Recomendados
Este guia cobre todo o ciclo de vida de uma REST API NestJS, mas cada secção pode tornar-se um aprofundamento à parte. Eis os temas naturalmente relacionados que vale a pena explorar a seguir:
- Modularizar um projeto NestJS de grande dimensão: feature modules, shared modules e barrel files.
- Dependency Injection no NestJS explicada com exemplos: providers customizados, factory providers, tokens de injeção.
- Autenticação JWT e Refresh Token em profundidade: token rotation, revogação e gestão multi-dispositivo.
- Role Based Access Control (RBAC) avançado: permissões granulares além dos simples roles.
- Validação com ValidationPipe e class-validator: validadores customizados e mensagens de erro localizadas.
- Logging centralizado no NestJS com Pino e correlação de pedidos através de request ID.
- Exception Filters personalizados para domínios específicos (ex. erros de pagamento, erros de domínio).
- Upload de ficheiros avançado com Multer: storage em S3, validação do conteúdo real dos ficheiros.
- Scheduler e Cron Jobs no NestJS com
@nestjs/schedule. - WebSocket com NestJS: notificações em tempo real e scaling com o Redis adapter.
- Testing de controllers e services NestJS: unit tests, mock de repositories, testes E2E com Supertest.
Conclusão
Construir uma REST API com NestJS significa investir numa estrutura que escala com a complexidade do projeto: módulos bem isolados, Dependency Injection para testabilidade, guards/interceptors/pipes para separar cross-cutting concerns da lógica de negócio, e um ecossistema — TypeORM, Passport, class-validator, Swagger — que cobre de forma madura todas as necessidades reais de produção. O exemplo de API de Blog construído neste guia (CRUD, JWT com refresh token, RBAC, upload de ficheiros, logging centralizado, gestão de erros) é o esqueleto reutilizável para a maioria das APIs NestJS que irás encontrar na prática.
Próximos passos recomendados: aprofunda a modularização de projetos NestJS de grande dimensão, a autenticação JWT e refresh token em detalhe, o padrão RBAC com permissões granulares, e as técnicas de testing para controllers e services — temas que completam naturalmente este guia.