O que é NestJS?
NestJS é um framework progressivo de Node.js que traz estrutura para o JavaScript no lado do servidor. Enquanto o Express fornece primitivos e deixa a escolha para você, o NestJS oferece uma arquitetura: módulos, controllers, providers, decorators e um container de injeção de dependência. Ele se inspira fortemente no Angular e em frameworks Java corporativos, rodando sobre o Express ou Fastify.
Essa postura opinativa é a base de sua proposta de valor. Em uma equipe de cinco pessoas construindo uma API robusta, os problemas mais difíceis raramente são relacionados ao HTTP. Eles envolvem nomenclatura, delimitação de fronteiras, testes e a manutenção de padrões consistentes à medida que a base de código cresce. O NestJS resolve isso com convenções que você pode aceitar ou substituir, e já entrega toda a infraestrutura — validação, guards, interceptors, configuração, utilitários de teste — para que você não precise montar tudo do zero.
Módulos definem as fronteiras
Tudo no NestJS é organizado em módulos. Um módulo é uma classe decorada com @Module que declara seus controllers, seus providers e o que ele exporta para o restante da aplicação.
@Module({
imports: [DatabaseModule],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
imports traz outros módulos cujos providers exportados você necessita. providers são as classes que este módulo possui. exports é a API pública: apenas o que um módulo exporta pode ser injetado em outro lugar. Isso torna as dependências explícitas — se OrdersModule precisa de CatsService, ele deve importar CatsModule, e o container (que funciona como um compilador) avisará quando não for possível resolver algo.
O AppModule raiz importa cada módulo de funcionalidade (feature module). Módulos de funcionalidade podem ser agrupados em módulos compartilhados (shared) ou core, e @Global() marca um módulo que deve estar disponível em todo lugar sem a necessidade de importações repetidas. Use módulos globais com moderação, apenas para infraestruturas genuinamente transversais, como configuração ou logging.
Controllers: roteamento como decorators
Um controller mapeia requisições HTTP para métodos. O class decorator define o caminho base, e os method decorators definem o verbo e o sub-caminho.
@Controller("cats")
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll() {
return this.catsService.findAll();
}
@Get(":id")
findOne(@Param("id") id: string) {
return this.catsService.findOne(id);
}
@Post()
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
}
Os parameter decorators realizam a extração: @Param, @Query, @Body, @Headers, @Req e @Res. O handler retorna um valor simples ou uma promise, e o NestJS o serializa para JSON com o status correto. Lance um HttpException, como o NotFoundException, e o framework o transformará em uma resposta de erro adequada.
Controllers devem ser “thin” (magros). Eles traduzem HTTP em uma chamada de método e nada mais. Toda a lógica pertence aos providers, o que mantém o controller testável e as preocupações de HTTP isoladas.
Providers e injeção de dependência
Um provider é qualquer classe que possa ser injetada. Marque-a como @Injectable() e declare-a em um módulo, e o container cuida do resto.
@Injectable()
export class CatsService {
constructor(private readonly db: DatabaseService) {}
findAll() {
return this.db.query("SELECT * FROM cats");
}
}
A injeção é feita via construtor e por tipo. O container lê os metadados emitidos, resolve cada parâmetro e passa a instância. Você também pode injetar por token quando o valor não for uma classe, utilizando @Inject("CONFIG") com um provider correspondente.
Providers podem ser customizados com factories, valores e aliases. Um factory provider é a maneira usual de injetar algo que precise de configuração assíncrona, como uma conexão de banco de dados:
@Module({
providers: [
{
provide: "DATABASE",
useFactory: async (config: ConfigService) => {
return createPool(config.getOrThrow("DATABASE_URL"));
},
inject: [ConfigService],
},
],
exports: ["DATABASE"],
})
export class DatabaseModule {}
O escopo também é importante. O escopo singleton padrão compartilha uma única instância em todo o app, que é o ideal para serviços stateless. O escopo REQUEST cria uma nova instância por requisição e é útil para contextos específicos da requisição, mas impacta a performance porque propaga por todo o grafo de dependências.
DTOs, pipes e validação
A validação de entrada é uma prioridade fundamental. Um DTO (data transfer object) é uma classe que descreve o formato de uma requisição, decorada com regras de class-validator.
export class CreateCatDto {
@IsString()
@MinLength(1)
name: string;
@IsInt()
@Min(0)
age: number;
}
Um pipe transforma ou valida o valor antes que ele chegue ao handler. Ative o ValidationPipe nativo globalmente e todo DTO decorado será verificado automaticamente:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
O whitelist remove propriedades desconhecidas, o forbidNonWhitelisted as rejeita sumariamente, e o transform converte os payloads em instâncias reais de DTO para que os tipos sejam reais em tempo de execução. Entradas inválidas geram um 400 estruturado sem a necessidade de escrever uma única linha de código de validação no handler. Pipes também podem ser aplicados por parâmetro ou por controller utilizando @UsePipes.
Guards, interceptors e filters
Esses três blocos de construção cobrem a maioria das preocupações transversais (cross-cutting concerns) e são executados em uma ordem fixa.
Guards são executados antes do handler e decidem se a requisição deve prosseguir.
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>("roles", context.getHandler());
if (!roles) return true;
const { user } = context.switchToHttp().getRequest();
return roles.includes(user?.role);
}
}
Interceptors envolvem a chamada do handler e podem transformar o resultado, medir o tempo de execução ou adicionar cache.
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const start = Date.now();
return next.handle().pipe(
tap(() => console.log(`took ${Date.now() - start}ms`)),
);
}
}
Exception filters capturam erros lançados e formatam a resposta. Sem um filtro, o NestJS utiliza um padrão razoável; um filtro customizado mantém o formato dos erros consistente em toda a API.
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}
Registre-os globalmente, por controller ou por rota. Guards, interceptors e filters globais são a maneira como uma aplicação NestJS madura centraliza autenticação, observabilidade e tratamento de erros.
Configuração e ambiente
O pacote @nestjs/config encapsula as variáveis de ambiente e as torna injetáveis. Importe-o uma única vez como um módulo global.
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}
Em seguida, injete ConfigService onde for necessário:
@Injectable()
export class DatabaseService {
constructor(private readonly config: ConfigService) {}
connect() {
const url = this.config.getOrThrow<string>("DATABASE_URL");
return createPool(url);
}
}
Dê preferência ao getOrThrow para valores obrigatórios, para que a ausência de uma variável cause uma falha na inicialização, em vez de ocorrer na primeira requisição. Combine isso com um esquema de validação para que a configuração seja verificada apenas uma vez, e mantenha os segredos totalmente fora do repositório.
Testando com TestingModule
As utilidades de teste do NestJS constroem um container real de forma isolada. Você declara apenas os providers que serão testados e, em seguida, substitui suas dependências por mocks.
describe("CatsController", () => {
let controller: CatsController;
let service: CatsService;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
controller = module.get(CatsController);
service = module.get(CatsService);
});
it("returns all cats", () => {
jest.spyOn(service, "findAll").mockReturnValue([]);
expect(controller.findAll()).toEqual([]);
});
});
Para testes end-to-end, construa a aplicação completa e utilize supertest contra app.getHttpServer(). Como o container de DI é o mesmo utilizado em produção, esses testes exercitam guards, pipes e filters exatamente como eles são implantados.
Melhores práticas
- Mantenha os controllers enxutos e coloque toda a lógica em services injetáveis.
- Defina limites de módulos em torno das funcionalidades e exporte apenas o que outros módulos precisarem.
- Valide cada requisição com um DTO e um
ValidationPipeglobal. - Use guards para autenticação e autorização, e interceptors para a formatação de respostas.
- Registre filtros de exceção globais para que as respostas de erro tenham um formato padronizado.
- Injete a configuração em vez de ler
process.envdiretamente. - Prefira o escopo singleton; utilize o escopo de requisição apenas quando for realmente necessário.
- Teste com
TestingModulee sobrescreva as dependências em vez de acessar serviços reais.
Erros comuns
- Construir providers com
newem vez de injetá-los, o que quebra o container. - Esquecer de adicionar um provider a um módulo e depois perder tempo tentando resolver um erro de “Nest can’t resolve dependencies”.
- Importar um módulo, mas esquecer de
exporto provider que ele deveria compartilhar. - Colocar lógica de negócio em controllers, tornando-os impossíveis de testar via unit test.
- Usar request scope em todo lugar e degradar a performance silenciosamente.
- Confiar em
@Body()sem um DTO e um validation pipe. - Espalhar a formatação de erros em vez de centralizá-la em um filter.
- Tratar módulos como pastas em vez de fronteiras de dependência.
Próximos passos
O NestJS é a maneira mais estruturada de construir um backend em Node.js e funciona diretamente sobre o Express ou Fastify. Leia esses guias para entender a camada HTTP que você está abstraindo. Um domínio sólido de TypeScript é essencial, pois os decorators e a injeção de dependência dependem de tipos, e os fundamentos de Node.js explicam o runtime onde tudo é executado. Depois, construa um módulo de funcionalidade de ponta a ponta: controller, service, DTO e teste.