Aula 5: Introdução ao NestJS
As aulas 1 a 4 prepararam o terreno: a arquitetura (01 e 02), a linguagem (03) e o contrato (04). Esta aula abre o Módulo 2 e começa a escrever o serviço que os clientes web e mobile vão consumir pelo resto do semestre.
O objetivo aqui não é entregar funcionalidade — é montar a estrutura certa para crescer. Quatro conceitos sustentam essa estrutura: módulos delimitam responsabilidades, controllers expõem endpoints, providers implementam regra de negócio e a injeção de dependência conecta as peças sem que uma precise construir a outra.
| O que vem depois | Onde |
|---|---|
| DTOs, validação, tratamento de erros e OpenAPI | Aula 6 — API REST com NestJS |
| Persistência com ORM e banco relacional | Aula 7 — Acesso a bases de dados |
| Autenticação, guards e proteção de rotas | Aula 8 — Autenticação e autorização |
Objetivos
Ao final desta aula, você deve ser capaz de:
- Justificar o uso de um framework opinativo em vez de um servidor HTTP mínimo, em termos de custo de manutenção.
- Descrever a função de cada arquivo do esqueleto gerado pelo Nest CLI.
- Organizar funcionalidades em módulos coesos e explicar o que
importseexportsautorizam. - Implementar um controller com rotas HTTP, parâmetros de caminho e códigos de status adequados.
- Criar providers para encapsular regra de domínio e injetá-los por construtor.
- Ordenar as etapas do pipeline de requisição do NestJS e decidir em qual delas uma regra deve morar.
- Relacionar os conceitos de NestJS com os equivalentes em Jakarta EE.
Ambiente sugerido
Os exemplos desta aula são executáveis. O laboratório no fim da página monta o projeto do zero.
| Ferramenta | Versão | Observação |
|---|---|---|
| Node.js | 22 LTS ou superior | node --version |
| npm | a que acompanha o Node | npm --version |
| NestJS | 11.x | instalado pelo CLI no passo 1 do laboratório |
| Cliente HTTP | curl, Insomnia, Postman ou a extensão REST Client | qualquer um serve |
A versão 12 do NestJS estava em preparação quando este material foi escrito e traz mudanças estruturais — migração completa para ESM, validação por Standard Schema e troca do conjunto de ferramentas padrão (Vitest no lugar do Jest, entre outras). O conteúdo desta aula — módulos, controllers, providers e injeção de dependência — não muda com ela. O que muda é sintaxe de importação e configuração do projeto. Confirme a versão adotada na turma antes de começar.
Parte 1 — Por que um framework, e não só um servidor HTTP
Node.js já sabe atender requisições sem ajuda de ninguém. Com Express, uma rota custa três linhas:
const express = require('express');
const app = express();
app.get('/livros', (req, res) => res.json(acervo));
app.listen(3000);
Se o problema é servir uma rota, isso basta. O problema aparece na décima quinta rota, quando o projeto tem quatro pessoas mexendo nele e três meses de histórico. Aí começam as perguntas que o Express não responde por conta própria:
- Onde mora a regra de negócio? (em qualquer lugar, é a resposta honesta)
- Como um módulo obtém o serviço de que precisa? (
requiredireto, criando acoplamento) - Como validar a entrada? (à mão, em cada rota, com risco de esquecer)
- Como testar uma rota sem subir o servidor inteiro? (com dificuldade)
O NestJS existe para responder essas perguntas da mesma forma em todo projeto. Ele não substitui o Express — por padrão roda sobre ele. O que ele acrescenta é uma arquitetura: uma convenção de onde cada coisa fica.
| O que o framework impõe | O que você ganha |
|---|---|
| Organização em módulos | Fronteiras explícitas entre funcionalidades |
| Injeção de dependência nativa | Classes que recebem colaboradores em vez de construí-los |
| Decorators para rotas e metadados | Contrato HTTP legível na própria assinatura do método |
| Pipeline padronizado (guards, pipes, interceptors, filtros) | Um lugar previsível para cada tipo de regra transversal |
| TypeScript de ponta a ponta | Erros de integração aparecem na compilação |
Frameworks opinativos cobram um custo real: você precisa aprender as convenções antes de ser produtivo, e sair delas dá trabalho. A troca compensa quando o projeto vive mais que algumas semanas e tem mais de um autor — que é exatamente o caso do estudo de caso desta disciplina. Para um script de uso único, o Express puro continua sendo a escolha certa.
Se você cursou Programação Web Java, esse raciocínio é familiar: é o mesmo argumento que justifica Jakarta EE ou Spring em vez de escrever servlets na mão.
Parte 2 — O esqueleto de um projeto
O Nest CLI gera esta estrutura mínima:
biblioteca-api/
├── src/
│ ├── main.ts ponto de entrada
│ ├── app.module.ts módulo raiz
│ ├── app.controller.ts controller de exemplo
│ └── app.service.ts provider de exemplo
├── test/ testes end-to-end
├── nest-cli.json configuração do CLI
├── tsconfig.json configuração do compilador
└── package.json
O ponto de entrada é um arquivo curto, e vale ler linha a linha:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// Percorre o grafo de módulos a partir da raiz, instancia todos os
// providers e monta o servidor HTTP.
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
void bootstrap();
NestFactory.create(AppModule) faz mais do que parece: ele lê os metadados de
todos os módulos alcançáveis a partir da raiz, resolve o grafo de
dependências e instancia cada provider na ordem correta. Se faltar uma
dependência, o erro aparece no boot, não na primeira requisição — o que é uma
característica desejável e será útil no laboratório.
src/main.ts não declara nenhum tipo, e mesmo assim está tipado. A inferência do
TypeScript (aula 3) carrega o peso aqui: app tem o tipo de retorno de
NestFactory.create, e o editor conhece todos os seus métodos.
Parte 3 — Módulos: a fronteira de visibilidade
Um módulo é uma classe anotada com @Module() que declara quatro coisas:
import { Module } from '@nestjs/common';
import { LivrosController } from './livros.controller';
import { LivrosService } from './livros.service';
import { LivrosStore } from './livros-store';
@Module({
imports: [], // o que este módulo consome
controllers: [LivrosController], // quem expõe HTTP
providers: [LivrosService, LivrosStore], // quem tem comportamento
exports: [LivrosService], // o que sai daqui
})
export class LivrosModule {}
A confusão mais comum é achar que módulo é sinônimo de pasta. Não é: módulo é
uma fronteira de visibilidade. Cada módulo tem seu próprio escopo de injeção, e
um provider só é visível fora dele se estiver em exports — e ainda assim só para
quem o declarar em imports.
Isso tem uma consequência prática que vale memorizar agora, porque ela custa tempo de depuração depois:
Se LivrosService aparecer na lista providers de dois módulos diferentes, cada
um terá a sua própria instância. Com dados em memória, isso significa dois
acervos independentes: você cadastra em um e busca no outro, e o registro
"sumiu". O padrão correto é declarar o provider em um módulo, exportá-lo, e
importar esse módulo onde ele for necessário.
Regras práticas para modularizar:
- um módulo representa um subdomínio (
livros,emprestimos,leitores), não um tipo técnico; - controllers ficam no limite HTTP e não conhecem regra;
- providers concentram regra e orquestração;
- exporte o mínimo: o que não sai não precisa manter compatibilidade.
Criar src/controllers/, src/services/ e src/repositories/ parece organizado
no começo e escala mal: mexer em uma funcionalidade passa a exigir abrir três
pastas distantes. Prefira coesão por funcionalidade — tudo de livros dentro de
src/livros/.
Parte 4 — Controllers: o limite HTTP
Um controller traduz HTTP em chamada de método e o retorno em resposta. É só isso — e essa modéstia é intencional.
import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common';
import { LivrosService } from './livros.service';
@Controller('livros')
export class LivrosController {
constructor(private readonly livrosService: LivrosService) {}
@Get()
listar(@Query('autor') autor?: string) {
return this.livrosService.listar(autor);
}
@Get(':id')
buscarPorId(@Param('id', ParseIntPipe) id: number) {
return this.livrosService.buscarPorId(id);
}
}
Quatro observações que respondem à maior parte das dúvidas iniciais:
@Controller('livros')define o prefixo da rota; os decorators de método completam o caminho.@Param('id', ParseIntPipe)extrai o parâmetro e o converte. Sem o pipe,idchegaria como a string"42"e a comparação com um número falharia silenciosamente.- O retorno é serializado em JSON automaticamente. Não existe
res.json()no caminho normal. - O controller não decide regra. Se aparecer um
ifde negócio aqui, ele está no lugar errado.
Status de resposta
Por padrão o NestJS responde 200 — exceto em @Post(), que responde 201.
Para qualquer outro caso, seja explícito:
import { Delete, HttpCode, HttpStatus, Param, ParseIntPipe } from '@nestjs/common';
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) // 204
remover(@Param('id', ParseIntPipe) id: number): void {
this.livrosService.remover(id);
}
Use a constante (HttpStatus.NO_CONTENT) em vez do número solto: o código passa a
dizer o que significa, não só qual é.
Parte 5 — Providers: onde a regra mora
Em NestJS, provider é qualquer classe que o contêiner sabe instanciar e
injetar: services, repositories, adapters, gateways, clientes de API externa. O
que os marca é o decorator @Injectable().
import { Injectable, NotFoundException } from '@nestjs/common';
import { LivrosStore } from './livros-store';
import { Livro } from './livro.model';
@Injectable()
export class LivrosService {
constructor(private readonly store: LivrosStore) {}
listar(autor?: string): Livro[] {
const todos = this.store.todos();
if (!autor) {
return todos;
}
const alvo = autor.toLowerCase();
return todos.filter((livro) => livro.autor.toLowerCase().includes(alvo));
}
buscarPorId(id: number): Livro {
const livro = this.store.porId(id);
if (!livro) {
throw new NotFoundException(`Livro ${id} não encontrado`);
}
return livro;
}
}
Note o que o service faz e o que ele não faz:
- decide a regra (o filtro por autor é insensível a maiúsculas — isso é uma decisão de negócio);
- sinaliza o erro em vocabulário HTTP (
NotFoundException), sem construir a resposta; - não sabe qual rota o chamou, nem se o cliente é web ou mobile;
- não sabe de onde vêm os dados — quem sabe é o
LivrosStore.
NotFoundException vem de @nestjs/common e parece contradizer a separação de
camadas. Na prática, é um meio-termo aceito na comunidade: a exceção descreve a
categoria do erro, e o framework a traduz em 404. Em projetos maiores é
comum lançar uma exceção de domínio (LivroInexistenteError) e traduzir num
exception filter — veremos essa técnica na aula 6.
O provider interno com os dados:
import { Injectable } from '@nestjs/common';
import { Livro } from './livro.model';
@Injectable()
export class LivrosStore {
private readonly livros: Livro[] = [
{ id: 1, titulo: 'Dom Casmurro', autor: 'Machado de Assis', ano: 1899 },
{ id: 2, titulo: 'Grande Sertão: Veredas', autor: 'Guimarães Rosa', ano: 1956 },
];
todos(): Livro[] {
return this.livros;
}
porId(id: number): Livro | undefined {
return this.livros.find((livro) => livro.id === id);
}
}
Separar Store de Service parece exagero com um array em memória. Não é: na
aula 7, o Store é substituído pelo acesso ao PostgreSQL e o Service
permanece igual. Essa é a razão de existir da separação, e ela só fica
visível quando a troca acontece.
Parte 6 — Injeção de dependência
A injeção de dependência (DI) é o mecanismo que faz constructor(private readonly store: LivrosStore) funcionar sem que ninguém escreva new.
Sem DI, cada classe constrói suas colaboradoras:
class LivrosService {
private store = new LivrosStore(); // acoplado à implementação concreta
}
Com DI, o contêiner as fornece:
class LivrosService {
constructor(private readonly store: LivrosStore) {}
}
A diferença parece cosmética e não é:
| Sem DI | Com DI |
|---|---|
| A classe decide qual implementação usar | A composição é decidida no módulo |
| Trocar a implementação exige editar a classe | Basta alterar o providers do módulo |
| Testar exige o colaborador real | O teste injeta um substituto |
Cada new cria uma instância | Por padrão, o provider é singleton no módulo |
O mecanismo por trás disso é o mesmo dos decorators visto na aula 3: o compilador emite metadados sobre os tipos do construtor, e o contêiner os lê no boot.
Se A injeta B e B injeta A, o contêiner não consegue decidir quem
instanciar primeiro e a aplicação falha no boot. Existe um contorno
(forwardRef()), mas ele é quase sempre o sintoma de uma fronteira mal
desenhada: se dois módulos precisam um do outro, provavelmente existe um terceiro
conceito escondido entre eles.
Parte 7 — O pipeline de requisição
Entre a requisição chegar e o método do controller executar, o NestJS roda uma sequência fixa de etapas. Nenhuma é obrigatória, mas a ordem é:
| Etapa | Pergunta que ela responde | Erro típico que produz |
|---|---|---|
| Middleware | O que fazer com toda requisição, antes de qualquer roteamento? | — |
| Guard | Quem está chamando pode fazer isso? | 401, 403 |
| Interceptor (antes) | O que envolve a chamada? (tempo, log, envelope) | — |
| Pipe | O argumento é válido e está no tipo certo? | 400 |
| Handler | Qual é a regra? | 404, 409 |
| Interceptor (depois) | Como transformar a resposta? | — |
| Exception filter | Como esta exceção vira corpo de erro? | qualquer |
A ordem carrega decisões de projeto que valem explicitar:
- O guard roda antes do pipe porque não faz sentido validar o corpo de uma requisição que será recusada por falta de permissão.
- O pipe roda antes do handler porque o método do controller deve poder assumir que o argumento já é válido — é o que permite escrever a regra sem
ifdefensivo. - O interceptor é o único que envolve os dois lados, e por isso é o lugar de medir tempo, padronizar envelope de resposta e registrar auditoria.
Nesta aula você usa uma etapa só — o ParseIntPipe, um pipe embutido. A aula 6
acrescenta o ValidationPipe e os filtros de exceção; a aula 8, os guards.
Parte 8 — Equivalências com Jakarta EE
Para quem cursou Programação Web Java, o mapa abaixo encurta o caminho. O padrão arquitetural é o mesmo; muda a sintaxe e o ecossistema.
| Jakarta EE | NestJS | Observação |
|---|---|---|
@Path em um resource | @Controller('rota') | Ambos definem o prefixo |
@GET, @POST, @Produces | @Get(), @Post() | O Nest serializa JSON por padrão |
@PathParam / @QueryParam | @Param() / @Query() | No Nest, o pipe também converte o tipo |
Bean CDI com @ApplicationScoped | Provider com @Injectable() | Singleton é o padrão nos dois |
@Inject | Injeção por construtor | O Nest dispensa o decorator no caso comum |
beans.xml / descoberta automática | @Module({ providers: [...] }) | O Nest exige registro explícito |
Bean Validation (@NotNull, @Email) | class-validator (@IsNotEmpty, @IsEmail) | Aula 6 |
| JPA / Hibernate | Prisma | Aula 7 |
ExceptionMapper | Exception filter | Aula 6 |
A diferença conceitual que mais surpreende: em Jakarta EE, a descoberta de beans é largamente automática; no NestJS, tudo é registrado explicitamente no módulo. É mais verboso e, em compensação, o grafo de dependências da aplicação está escrito em algum lugar que dá para ler.
Erros comuns
| Erro | Sintoma | Correção |
|---|---|---|
Esquecer @Injectable() no provider | Nest can't resolve dependencies no boot | Anotar a classe |
Injetar um provider sem exports no módulo de origem | O mesmo erro, mencionando o módulo | Exportar lá, importar aqui |
| Declarar o mesmo provider em dois módulos | Estado "some": duas instâncias independentes | Declarar em um só e exportar |
Esquecer de registrar o módulo em imports do AppModule | Rota responde 404 e nada nos logs | Acrescentar aos imports |
Usar @Param('id') sem ParseIntPipe | Comparação com número nunca casa | Acrescentar o pipe |
| Colocar regra de negócio no controller | Regra duplicada quando surge a segunda rota | Mover para o service |
Rota :id declarada antes de uma rota fixa | GET /livros/busca cai no handler de :id | Declarar as rotas fixas primeiro |
Retornar res.json() manualmente | Perde-se a serialização e os interceptors | Retornar o objeto |
O NestJS resolve rotas na ordem em que os métodos aparecem na classe. Se
@Get(':id') vier antes de @Get('disponiveis'), a requisição
GET /livros/disponiveis casa com o primeiro, e ParseIntPipe responde 400
tentando converter "disponiveis" em número. Rotas fixas sempre antes das
paramétricas.
Laboratório 5 — A base do serviço
O laboratório dos três primeiros módulos usa um domínio-guia único: uma biblioteca. Ele foi escolhido pequeno o bastante para caber em uma aula e completo o bastante para exigir relacionamentos de verdade na aula 7.
Você vai construir esse domínio-guia passo a passo. Em paralelo, cada etapa traz um bloco No seu projeto, indicando como traduzir aquele passo para o domínio que você escolheu para o seu estudo de caso. Faça os dois: o domínio-guia garante que você tem uma referência funcionando para comparar; o seu projeto é o que consolida o aprendizado.
Resultado ao fim deste laboratório: uma API que responde a duas rotas de leitura, com dados em memória, organizada em módulos e com regra separada do transporte.
Passo 1 — Conferir o ambiente
node --version # precisa ser 22.x ou superior
npm --version
Se a versão do Node for anterior à 22, atualize antes de continuar — o NestJS 11 não suporta versões antigas e o erro que ele produz não é óbvio.
Passo 2 — Criar o projeto
npm i -g @nestjs/cli
nest new biblioteca-api
O CLI pergunta o gerenciador de pacotes; escolha npm (ou o que a turma tiver
combinado — só não misture os dois no mesmo projeto).
cd biblioteca-api
npm run start:dev
Abra http://localhost:3000. Deve aparecer Hello World!. Deixe este comando
rodando: start:dev observa os arquivos e reinicia sozinho a cada gravação.
Altere a chamada em src/main.ts para await app.listen(3001) e use essa porta
no restante do laboratório.
Passo 3 — Ler o esqueleto antes de mexer
Antes de escrever qualquer coisa, abra os quatro arquivos de src/ e responda
para si mesmo:
- Em
app.module.ts, o que está listado emcontrollerse emproviders? - Em
app.controller.ts, qual rota o decorator@Get()cria, considerando que@Controller()está sem argumento? - Em
app.service.ts, o que@Injectable()marca? - Em
main.ts, em que linha o servidor passa a aceitar conexões?
Esse exercício de leitura vale mais do que parece: o esqueleto é pequeno o bastante para ser inteiramente compreendido, e é a última vez no semestre em que isso será verdade.
Passo 4 — Limpar o esqueleto
O controller e o service de exemplo não fazem parte do domínio. Remova-os:
rm src/app.controller.ts src/app.service.ts src/app.controller.spec.ts
E deixe o módulo raiz vazio:
import { Module } from '@nestjs/common';
@Module({
imports: [],
})
export class AppModule {}
O terminal do start:dev vai acusar erro de compilação enquanto os arquivos
antigos estiverem referenciados — é esperado, e some quando o módulo raiz for
salvo.
Passo 5 — Gerar o módulo de livros
nest g module livros
nest g service livros --no-spec
nest g controller livros --no-spec
Confira o que o CLI fez sozinho: ele criou src/livros/, registrou
LivrosModule em imports do AppModule e registrou o controller e o service
em LivrosModule. Abra os dois módulos e confirme — entender que essas
declarações existem é mais importante do que tê-las digitado.
A opção --no-spec omite os arquivos de teste. Testes automatizados entram mais
adiante no semestre; por ora eles só adicionariam ruído.
nest g resourceO CLI tem um comando (nest g resource) que gera módulo, controller, service,
DTOs e um CRUD inteiro de uma vez. Ele é excelente quando você já sabe o que ele
gera — e péssimo agora, porque entregaria pronto exatamente aquilo que esta aula
quer que você entenda. Depois do laboratório 6, use-o à vontade.
Passo 6 — Modelar o recurso
Crie o tipo que representa um livro:
export interface Livro {
id: number;
titulo: string;
autor: string;
ano: number;
}
Uma interface basta aqui porque este tipo só existe em tempo de compilação — não
há validação nem serialização dependendo dele. Na aula 6, o DTO de entrada será
uma class, e a razão dessa diferença ficará clara.
Escolha o recurso principal do seu estudo de caso — aquele que o usuário
enxerga primeiro e do qual os outros dependem. Modele-o com quatro ou cinco
campos, sem relacionamentos ainda. Se o seu domínio é uma pizzaria, este é
Pizza, não Pedido.
Passo 7 — Criar o provider de dados
nest g provider livros/livros-store --flat --no-spec
O CLI cria src/livros/livros-store.ts com a classe LivrosStore — sem
sufixo nenhum. Essa é a diferença entre os dois geradores: nest g service
acrescenta Service ao nome do arquivo e da classe, enquanto nest g provider
usa exatamente o nome que você escreveu. É o que queremos aqui, porque este
provider não tem regra de negócio, só guarda dados — chamá-lo de
LivrosStoreService seria enganoso. A opção --flat garante que o arquivo
fique direto em src/livros/, sem uma subpasta só para ele.
import { Injectable } from '@nestjs/common';
import { Livro } from './livro.model';
@Injectable()
export class LivrosStore {
private readonly livros: Livro[] = [
{ id: 1, titulo: 'Dom Casmurro', autor: 'Machado de Assis', ano: 1899 },
{ id: 2, titulo: 'Grande Sertão: Veredas', autor: 'Guimarães Rosa', ano: 1956 },
{ id: 3, titulo: 'A Hora da Estrela', autor: 'Clarice Lispector', ano: 1977 },
];
todos(): Livro[] {
return this.livros;
}
porId(id: number): Livro | undefined {
return this.livros.find((livro) => livro.id === id);
}
}
Confirme que LivrosStore está listada em providers no LivrosModule — o CLI
a registra automaticamente, e é essa declaração que permite injetá-la no service
do próximo passo.
Passo 8 — Implementar o service
import { Injectable, NotFoundException } from '@nestjs/common';
import { LivrosStore } from './livros-store';
import { Livro } from './livro.model';
@Injectable()
export class LivrosService {
constructor(private readonly store: LivrosStore) {}
listar(autor?: string): Livro[] {
const todos = this.store.todos();
if (!autor) {
return todos;
}
const alvo = autor.toLowerCase();
return todos.filter((livro) => livro.autor.toLowerCase().includes(alvo));
}
buscarPorId(id: number): Livro {
const livro = this.store.porId(id);
if (!livro) {
throw new NotFoundException(`Livro ${id} não encontrado`);
}
return livro;
}
}
Passo 9 — Implementar o controller
import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common';
import { LivrosService } from './livros.service';
@Controller('livros')
export class LivrosController {
constructor(private readonly livrosService: LivrosService) {}
@Get()
listar(@Query('autor') autor?: string) {
return this.livrosService.listar(autor);
}
@Get(':id')
buscarPorId(@Param('id', ParseIntPipe) id: number) {
return this.livrosService.buscarPorId(id);
}
}
Passo 10 — Executar e testar
Com npm run start:dev rodando, exercite as quatro situações:
# lista completa — 200 com três livros
curl -i http://localhost:3000/livros
# filtro por autor — 200 com um livro
curl -i "http://localhost:3000/livros?autor=clarice"
# busca por id existente — 200
curl -i http://localhost:3000/livros/2
# busca por id inexistente — 404 com a mensagem do service
curl -i http://localhost:3000/livros/99
A opção -i mostra os cabeçalhos, e é o que permite conferir o status, não só
o corpo. Confira que o quarto comando devolve 404 e um corpo parecido com:
{
"message": "Livro 99 não encontrado",
"error": "Not Found",
"statusCode": 404
}
Se ele devolveu 200 com corpo vazio, o throw não está sendo executado — o
service provavelmente está retornando undefined em vez de lançar.
Passo 11 — Provar o encapsulamento
Este passo é o coração da aula. Ele provoca dois defeitos de propósito: o primeiro derruba a aplicação e se anuncia sozinho; o segundo deixa a aplicação subir normalmente e não aparece em lugar nenhum — por isso ele precisa de instrumentação para ser observado. Faça os dois na ordem.
11.1 — Criar o segundo módulo
nest g module emprestimos
nest g service emprestimos --no-spec
11.2 — Provocar o erro visível
Injete o LivrosService no EmprestimosService:
import { Injectable } from '@nestjs/common';
import { LivrosService } from '../livros/livros.service';
@Injectable()
export class EmprestimosService {
constructor(private readonly livrosService: LivrosService) {}
descreverLivro(id: number): string {
const livro = this.livrosService.buscarPorId(id);
return `${livro.titulo} (${livro.ano})`;
}
}
Salve e olhe o terminal. A aplicação não sobe, e a mensagem é parecida com:
ERROR [ExceptionHandler] UnknownDependenciesException [Error]:
Nest can't resolve dependencies of the EmprestimosService (?).
Please make sure that the argument LivrosService at index [0] is available
in the EmprestimosModule module.
Potential solutions:
- Is EmprestimosModule a valid NestJS module?
- If LivrosService is a provider, is it part of the current EmprestimosModule?
- If LivrosService is exported from a separate @Module, is that module imported
within EmprestimosModule?
Verificação: o boot falha e a mensagem cita as três coisas que importam — a
classe que não pôde ser construída (EmprestimosService), o argumento que
faltou (LivrosService at index [0]) e o módulo onde ele não estava visível
(EmprestimosModule). Leia a mensagem com atenção antes de corrigir: ela diz
exatamente o que falta e onde.
11.3 — Corrigir pelo caminho certo
A correção tem duas metades, uma em cada módulo:
@Module({
controllers: [LivrosController],
providers: [LivrosService, LivrosStore],
exports: [LivrosService], // 1. autoriza a saída
})
export class LivrosModule {}
import { Module } from '@nestjs/common';
import { LivrosModule } from '../livros/livros.module';
import { EmprestimosService } from './emprestimos.service';
@Module({
imports: [LivrosModule], // 2. autoriza a entrada
providers: [EmprestimosService],
})
export class EmprestimosModule {}
Verificação: a aplicação volta a subir e as rotas do passo 10 continuam
respondendo. O EmprestimosService ainda não tem rota nenhuma — a prova de que
ele foi construído é o boot ter passado, porque o Nest instancia os providers de
um módulo durante a inicialização, e não na primeira requisição.
11.4 — Instrumentar o store
O próximo defeito é silencioso: ele não derruba a aplicação nem escreve nada no
log. Para conseguir observá-lo, torne cada instância do store visível. Acrescente
ao LivrosStore um contador e uma mensagem de construção:
@Injectable()
export class LivrosStore {
// instrumentação temporária — remover ao fim do passo 11
private static contador = 0;
private readonly numero = ++LivrosStore.contador;
constructor() {
console.log(`[LivrosStore] instância #${this.numero} criada`);
}
private readonly livros: Livro[] = [
// ... inalterado
];
// ... todos() e porId() inalterados
}
O campo contador é static: ele pertence à classe, não a cada objeto, então
sobrevive entre as construções e conta quantas vezes o Nest instanciou o store no
processo.
Verificação: reinicie e confira que o log de boot traz exatamente uma
linha [LivrosStore] instância #1 criada. Um acervo, uma instância — é assim que
tem de ser.
11.5 — A experiência errada
Agora abandone o exports/imports e declare o provider diretamente no módulo
que precisa dele, que é a tentação de quem quer "resolver rápido" o erro de 11.2:
@Module({
// sem imports
providers: [EmprestimosService, LivrosService],
})
export class EmprestimosModule {}
Primeira surpresa: a aplicação não sobe, e o erro mudou de alvo:
ERROR [ExceptionHandler] UnknownDependenciesException [Error]:
Nest can't resolve dependencies of the LivrosService (?).
Please make sure that the argument LivrosStore at index [0] is available
in the EmprestimosModule module.
Potential solutions:
- Is EmprestimosModule a valid NestJS module?
- If LivrosStore is a provider, is it part of the current EmprestimosModule?
- If LivrosStore is exported from a separate @Module, is that module imported
within EmprestimosModule?
É a mesma mensagem de 11.2 com outro sujeito: antes faltava o
LivrosService, agora falta o LivrosStore. E faz sentido — LivrosService
depende de LivrosStore, que continua invisível dentro do EmprestimosModule.
Declarar um provider em outro módulo arrasta junto toda a cadeia de
dependências dele. Esse já é o primeiro sinal de
que o caminho está errado — mas siga em frente, porque o objetivo é ver aonde ele
leva. Duplique também o store:
@Module({
providers: [EmprestimosService, LivrosService, LivrosStore],
})
export class EmprestimosModule {}
Verificação: agora a aplicação sobe, sem erro nenhum — e o log traz duas linhas:
[LivrosStore] instância #1 criada
[LivrosStore] instância #2 criada
São dois acervos independentes na memória. O LivrosController conversa com o
primeiro, porque resolve o LivrosService dentro do LivrosModule; o
EmprestimosService conversa com o segundo, porque resolve a cópia declarada no
EmprestimosModule. Nada no terminal indica problema.
Enquanto tudo for leitura sobre a mesma lista fixa, as duas instâncias se
comportam de forma idêntica e o defeito fica escondido — que é exatamente o que
o torna caro. Ele aparece na primeira escrita: na aula 6, um POST /livros
gravaria no acervo do LivrosController, e uma consulta feita pelo
EmprestimosService leria o outro acervo e não encontraria o registro. É o
"sumiu" que a Parte 3 antecipou: sem exceção, sem stack trace, só um resultado
inexplicável.
Sempre que um estado em memória se comportar de forma inconsistente entre dois pontos da aplicação, instrumente o construtor do provider como em 11.4. Duas linhas no boot respondem em segundos uma pergunta que se arrasta por horas.
11.6 — Desfazer
Volte o EmprestimosModule para a versão de 11.3 — imports: [LivrosModule] e
providers: [EmprestimosService] — e confirme que o log voltou a mostrar uma
única instância. Só então remova a instrumentação do LivrosStore: o contador, o
campo numero e o construtor. Ela cumpriu o papel e não pertence ao código
final.
Identifique dois recursos do seu domínio em que um depende do outro (pedido
depende de produto, matrícula depende de turma, empréstimo depende de livro).
Crie os dois módulos e conecte-os por exports/imports. Você não precisa
implementar a regra ainda — basta a dependência existir e a aplicação subir.
Critérios de conclusão
O laboratório está completo quando:
-
npm run start:devsobe sem erros e sem avisos de dependência; -
GET /livrosdevolve200com a lista; -
GET /livros?autor=...filtra corretamente, sem diferenciar maiúsculas; -
GET /livros/2devolve200eGET /livros/99devolve404; -
GET /livros/abcdevolve400(efeito doParseIntPipe); - o controller não contém nenhuma regra de negócio;
-
EmprestimosServiceusaLivrosServiceporexports/imports, e não por declaração duplicada; - você executou a experiência errada do passo 11.5, viu as duas instâncias no log e desfez a alteração;
- a instrumentação temporária do
LivrosStorefoi removida; - o mesmo esqueleto existe no seu projeto, com o seu domínio.
Fechamento
A entrega desta aula não é funcionalidade — é forma. Sem essa estrutura, cada rota nova tende a misturar transporte, regra e acesso a dados no mesmo arquivo, e o custo disso não aparece na primeira semana: aparece quando a regra precisa mudar em três lugares ao mesmo tempo.
Com módulos, controllers, providers e DI no lugar, as próximas aulas encaixam
sem reescrita: DTOs e validação (aula 6) entram no limite HTTP, persistência
(aula 7) substitui o Store sem tocar no Service, e autenticação (aula 8) entra
como uma etapa do pipeline que você já sabe onde fica.
Exercícios (Checkpoints)
-
Explique, em uma frase cada, a responsabilidade de
LivrosController,LivrosServiceeLivrosStore. Depois indique qual dos três precisaria mudar em cada situação: (a) a rota passa de/livrospara/acervo/livros; (b) o filtro por autor passa a ignorar acentos; (c) os dados passam a vir de um banco. -
Diagnostique: uma aplicação sobe normalmente, mas um livro cadastrado por um endpoint não aparece na listagem de outro módulo. Qual é a causa mais provável e como você a confirmaria sem abrir o depurador?
-
Sobre módulos:
a. Descreva o que acontece se
LivrosServiceestiver emprovidersmas não emexports, e outro módulo tentar injetá-lo.b. Justifique por que exportar apenas o necessário é preferível a exportar tudo, mesmo em um projeto pequeno.
-
Ordene as etapas do pipeline de requisição e indique em qual delas colocaria cada regra, justificando: (a) rejeitar requisições sem token; (b) recusar um corpo com campo faltando; (c) registrar quanto tempo cada rota levou; (d) converter qualquer exceção não tratada num corpo de erro padronizado.
-
Justifique por que
@Get('disponiveis')deve ser declarado antes de@Get(':id'), e descreva o status e o corpo que o cliente receberia se a ordem fosse invertida. -
Compare o trecho
constructor(private readonly store: LivrosStore)comprivate store = new LivrosStore(). Descreva uma situação concreta de teste em que a primeira forma economiza trabalho. -
Implemente no laboratório uma rota
GET /livros/decada/:decadaque devolva os livros publicados naquela década (por exemplo,/livros/decada/1950retorna os de 1950 a 1959). Indique onde cada parte foi escrita e por quê. -
Relacione cada conceito de NestJS ao seu equivalente em Jakarta EE e aponte uma diferença de comportamento em cada par:
@Injectable(),@Controller,@Module, injeção por construtor.
Referências
Principais
- NestJS — First steps — instalação e esqueleto do projeto
- NestJS — Modules — escopo,
imports,exportse módulos compartilhados - NestJS — Controllers — rotas, parâmetros e códigos de status
- NestJS — Providers —
@Injectable()e injeção de dependência - NestJS — CLI overview — comandos de geração de código
Aprofundamento
- NestJS — Custom providers — tokens, fábricas e providers assíncronos
- NestJS — Injection scopes — quando o singleton padrão não serve
- NestJS — Circular dependency —
forwardRef()e por que evitá-lo - NestJS — Request lifecycle — a ordem completa do pipeline
- Express — Guia de roteamento — a camada sobre a qual o NestJS roda por padrão