Pular para o conteúdo principal

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 depoisOnde
DTOs, validação, tratamento de erros e OpenAPIAula 6 — API REST com NestJS
Persistência com ORM e banco relacionalAula 7 — Acesso a bases de dados
Autenticação, guards e proteção de rotasAula 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 imports e exports autorizam.
  • 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.

FerramentaVersãoObservação
Node.js22 LTS ou superiornode --version
npma que acompanha o Nodenpm --version
NestJS11.xinstalado pelo CLI no passo 1 do laboratório
Cliente HTTPcurl, Insomnia, Postman ou a extensão REST Clientqualquer um serve
E a versão 12?

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? (require direto, 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õeO que você ganha
Organização em módulosFronteiras explícitas entre funcionalidades
Injeção de dependência nativaClasses que recebem colaboradores em vez de construí-los
Decorators para rotas e metadadosContrato 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 pontaErros de integração aparecem na compilação
O preço da convençã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:

src/main.ts
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.

Onde estão os tipos

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:

src/livros/livros.module.ts
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.

Dois módulos lado a lado: LivrosModule contém controller, service e store; EmprestimosModule enxerga o LivrosService porque ele foi exportado, mas não enxerga o LivrosStore, que ficou apenas em providers

Isso tem uma consequência prática que vale memorizar agora, porque ela custa tempo de depuração depois:

Registrar o mesmo provider em dois módulos cria duas instâncias

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.
Módulo não é pasta por tipo

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.

src/livros/livros.controller.ts
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, id chegaria 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 if de 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().

src/livros/livros.service.ts
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.
Por que o service lança uma exceção "HTTP"

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:

src/livros/livros-store.ts
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 DICom DI
A classe decide qual implementação usarA composição é decidida no módulo
Trocar a implementação exige editar a classeBasta alterar o providers do módulo
Testar exige o colaborador realO teste injeta um substituto
Cada new cria uma instânciaPor 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.

Fluxo em três etapas: o código com decorators é compilado, o compilador emite metadados de rota e de dependências, e no boot o contêiner do NestJS lê esses metadados, resolve o grafo e injeta as instâncias no construtor
A dependência circular

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 é:

Caminho de entrada de uma requisição no NestJS: middleware, guard, interceptor, pipe e handler; qualquer exceção lançada desvia para o exception filter, e o caminho de saída passa pelo interceptor antes de virar resposta
EtapaPergunta que ela respondeErro típico que produz
MiddlewareO que fazer com toda requisição, antes de qualquer roteamento?
GuardQuem está chamando pode fazer isso?401, 403
Interceptor (antes)O que envolve a chamada? (tempo, log, envelope)
PipeO argumento é válido e está no tipo certo?400
HandlerQual é a regra?404, 409
Interceptor (depois)Como transformar a resposta?
Exception filterComo 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 if defensivo.
  • 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 EENestJSObservaçã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 @ApplicationScopedProvider com @Injectable()Singleton é o padrão nos dois
@InjectInjeção por construtorO 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 / HibernatePrismaAula 7
ExceptionMapperException filterAula 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

ErroSintomaCorreção
Esquecer @Injectable() no providerNest can't resolve dependencies no bootAnotar a classe
Injetar um provider sem exports no módulo de origemO mesmo erro, mencionando o móduloExportar lá, importar aqui
Declarar o mesmo provider em dois módulosEstado "some": duas instâncias independentesDeclarar em um só e exportar
Esquecer de registrar o módulo em imports do AppModuleRota responde 404 e nada nos logsAcrescentar aos imports
Usar @Param('id') sem ParseIntPipeComparação com número nunca casaAcrescentar o pipe
Colocar regra de negócio no controllerRegra duplicada quando surge a segunda rotaMover para o service
Rota :id declarada antes de uma rota fixaGET /livros/busca cai no handler de :idDeclarar as rotas fixas primeiro
Retornar res.json() manualmentePerde-se a serialização e os interceptorsRetornar o objeto
A ordem de declaração das rotas importa

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.

Se a porta 3000 estiver ocupada

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:

  1. Em app.module.ts, o que está listado em controllers e em providers?
  2. Em app.controller.ts, qual rota o decorator @Get() cria, considerando que @Controller() está sem argumento?
  3. Em app.service.ts, o que @Injectable() marca?
  4. 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:

src/app.module.ts
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.

Por que não nest g resource

O 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:

src/livros/livro.model.ts
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.

No seu projeto

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.

src/livros/livros-store.ts
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

src/livros/livros.service.ts
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

src/livros/livros.controller.ts
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:

src/emprestimos/emprestimos.service.ts
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:

src/livros/livros.module.ts
@Module({
controllers: [LivrosController],
providers: [LivrosService, LivrosStore],
exports: [LivrosService], // 1. autoriza a saída
})
export class LivrosModule {}
src/emprestimos/emprestimos.module.ts
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:

src/livros/livros-store.ts
@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:

src/emprestimos/emprestimos.module.ts
@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:

src/emprestimos/emprestimos.module.ts
@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.

O mesmo teste serve para o seu projeto

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.

No seu projeto

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:dev sobe sem erros e sem avisos de dependência;
  • GET /livros devolve 200 com a lista;
  • GET /livros?autor=... filtra corretamente, sem diferenciar maiúsculas;
  • GET /livros/2 devolve 200 e GET /livros/99 devolve 404;
  • GET /livros/abc devolve 400 (efeito do ParseIntPipe);
  • o controller não contém nenhuma regra de negócio;
  • EmprestimosService usa LivrosService por exports/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 LivrosStore foi 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)

  1. Explique, em uma frase cada, a responsabilidade de LivrosController, LivrosService e LivrosStore. Depois indique qual dos três precisaria mudar em cada situação: (a) a rota passa de /livros para /acervo/livros; (b) o filtro por autor passa a ignorar acentos; (c) os dados passam a vir de um banco.

  2. 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?

  3. Sobre módulos:

    a. Descreva o que acontece se LivrosService estiver em providers mas não em exports, 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.

  4. 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.

  5. 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.

  6. Compare o trecho constructor(private readonly store: LivrosStore) com private store = new LivrosStore(). Descreva uma situação concreta de teste em que a primeira forma economiza trabalho.

  7. Implemente no laboratório uma rota GET /livros/decada/:decada que devolva os livros publicados naquela década (por exemplo, /livros/decada/1950 retorna os de 1950 a 1959). Indique onde cada parte foi escrita e por quê.

  8. 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

Aprofundamento