Identificação
- Módulo: 5 — Node.js, NestJS e APIs
- Duração: 2 horas
- Tipo: teoria aplicada e laboratório
- Entrega:
TasksModuleorganizado com controlador, serviço e repositório em memória - Laboratório:
../exemplos/aula-5.2/index.html
Introdução
Na aula anterior criamos uma aplicação NestJS e um endpoint de saúde. Agora surge
uma dúvida comum: se tudo pode ser escrito em um único arquivo, por que separar o
código em módulos, controladores (controllers) e provedores (providers)?
Uma aplicação pequena até cabe em um arquivo. Porém, quando tarefas, usuários, documentos e integrações começam a crescer, misturar rotas, regras e armazenamento torna qualquer mudança perigosa. O NestJS fornece uma forma explícita de organizar essas responsabilidades.
Antes de programar, responda mentalmente:
- módulo é apenas uma pasta?
- controller deve decidir todas as regras?
- provider é sempre um service?
- injeção de dependência significa instalar uma biblioteca?
importsdo TypeScript é igual aoimportsdo@Module()?- uma interface TypeScript pode ser usada como token em ambiente de execução?
Ao final, essas diferenças estarão visíveis no código e no laboratório.
Objetivos
Ao concluir a aula, você será capaz de:
- explicar módulo, controlador, provedor e injeção de dependência;
- diferenciar importação de arquivo e importação de módulo NestJS;
- organizar uma capacidade de negócio em um feature module;
- manter regras de negócio fora do controller;
- registrar e resolver providers pelo contêiner de IoC;
- usar classe, string ou
Symbolcomo token de injeção; - explicar encapsulamento,
importseexports; - reconhecer provider ausente e dependência circular;
- comparar os escopos default, requisição e transient;
- implementar um repositório em memória substituível.
Pré-requisitos
- Aula 5.1 concluída;
- noções iniciais de classe, interface e construtor em TypeScript;
- saber que uma requisição chega por uma rota HTTP;
- Node.js e npm disponíveis apenas para executar o exemplo real.
O laboratório visual funciona diretamente no XAMPP e não exige servidor Node.
1. O problema de colocar tudo no controller
Considere este código:
@Controller('tasks')
export class TasksController {
private readonly tasks = [];
@Get()
list() {
return this.tasks;
}
}
Ele funciona, mas o controller agora conhece HTTP, armazenamento e regras. Isso dificulta testes, reaproveitamento e a futura troca da memória pelo MySQL.
Adotaremos este fluxo:
requisição HTTP
→ TasksController
→ TasksService
→ TASK_REPOSITORY
→ InMemoryTasksRepository
Cada parte tem uma responsabilidade clara.
2. O que é um módulo?
No NestJS, um módulo é uma classe decorada com @Module(). O decorator fornece
metadados para o framework montar o grafo da aplicação.
@Module({
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule {}
Um módulo não é apenas uma pasta. A pasta organiza arquivos; a classe decorada declara ao NestJS o que pertence àquela parte da aplicação.
Toda aplicação possui um módulo raiz. O AppModule é o ponto inicial usado pelo
NestJS para descobrir os outros módulos.
3. Feature module
Um feature module agrupa elementos relacionados a uma capacidade do produto.
src/
├── app.module.ts
└── tasks/
├── domain/
│ └── task.ts
├── repositories/
│ ├── tasks-repository.ts
│ └── in-memory-tasks.repository.ts
├── tasks.controller.ts
├── tasks.service.ts
└── tasks.module.ts
O nome TasksModule representa a capacidade de administrar tarefas, não uma camada
genérica chamada “controllers” ou “services”. Essa organização aproxima arquivos
que mudam pelo mesmo motivo.
4. O que o @Module() recebe?
As propriedades principais são:
| Propriedade | Função |
|---|---|
controllers |
controllers HTTP pertencentes ao módulo |
providers |
dependências que o contêiner pode criar ou fornecer |
imports |
módulos que exportam dependências necessárias aqui |
exports |
providers que formam a interface pública do módulo |
@Module({
imports: [],
controllers: [TasksController],
providers: [TasksService],
exports: [TasksService],
})
export class TasksModule {}
Não exporte tudo automaticamente. Um provider não exportado permanece encapsulado e só pode ser usado dentro do módulo que o registrou.
5. import TypeScript versus imports do NestJS
Estas linhas cumprem trabalhos diferentes:
import { TasksModule } from './tasks/tasks.module';
@Module({
imports: [TasksModule],
})
export class AppModule {}
O primeiro import permite que o arquivo TypeScript referencie a classe. O array
imports informa ao contêiner NestJS que o módulo faz parte do grafo da aplicação.
Um não substitui o outro.
6. O que é um controller?
Controller é a porta de entrada HTTP. Ele deve:
- declarar rotas e verbos;
- ler parâmetros, cabeçalhos e corpo;
- delegar trabalho;
- devolver a resposta adequada.
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
list(): Task[] {
return this.tasksService.list();
}
}
O controller não cria o service com new TasksService(). Ele declara que precisa
da dependência e o NestJS a entrega.
7. O que é um provider?
Provider é qualquer valor que pode ser gerenciado e injetado pelo contêiner do NestJS. Services, repositórios, factories, configurações e adaptadores podem ser providers.
@Injectable()
export class TasksService {
list(): Task[] {
return [];
}
}
@Injectable() adiciona os metadados necessários para a classe participar do
sistema de injeção. O nome “service” é uma convenção de responsabilidade, não um
tipo especial diferente de provider.
8. Injeção de dependência sem mistério
Sem injeção:
const repository = new InMemoryTasksRepository();
const service = new TasksService(repository);
const controller = new TasksController(service);
Com o contêiner:
constructor(private readonly tasksService: TasksService) {}
O NestJS lê o token solicitado, procura o registro, cria as dependências na ordem correta e entrega a instância. Isso é inversão de controle: a classe informa do que precisa, mas não controla a montagem completa.
9. Token de injeção
O contêiner funciona como um mapa:
token → provider
Quando a própria classe é usada:
providers: [TasksService]
Isso equivale conceitualmente a:
providers: [
{ provide: TasksService, useClass: TasksService },
]
O token é TasksService e a implementação também.
10. Por que uma interface não basta como token?
Interfaces TypeScript desaparecem quando o código vira JavaScript. Portanto, o NestJS não consegue procurar uma interface em ambiente de execução.
export interface TasksRepository {
findAll(): Task[];
}
Criamos um token que existe em JavaScript:
export const TASK_REPOSITORY = Symbol('TASK_REPOSITORY');
E fazemos a associação:
{
provide: TASK_REPOSITORY,
useClass: InMemoryTasksRepository,
}
11. Implementando o domínio
export interface Task {
readonly id: string;
readonly title: string;
readonly completed: boolean;
}
O domínio não precisa conhecer decorators HTTP, NestJS ou MySQL.
12. Contrato do repositório
import type { Task } from '../domain/task';
export const TASK_REPOSITORY = Symbol('TASK_REPOSITORY');
export interface TasksRepository {
findAll(): Task[];
add(title: string): Task;
}
Na Aula 6, outra classe poderá implementar o mesmo contrato usando MySQL e Prisma. O controller não precisará ser reescrito por causa dessa troca.
13. Repositório em memória
@Injectable()
export class InMemoryTasksRepository implements TasksRepository {
private readonly tasks: Task[] = [
{ id: 'task-1', title: 'Estudar módulos NestJS', completed: false },
];
findAll(): Task[] {
return this.tasks.map((task) => ({ ...task }));
}
add(title: string): Task {
const task = {
id: `task-${this.tasks.length + 1}`,
title,
completed: false,
} as const;
this.tasks.push(task);
return { ...task };
}
}
Retornar cópias reduz o risco de outro componente alterar o array interno sem passar pelas regras do repositório.
14. Service com regra de aplicação
@Injectable()
export class TasksService {
constructor(
@Inject(TASK_REPOSITORY)
private readonly repository: TasksRepository,
) {}
list(): Task[] {
return this.repository.findAll();
}
create(title: string): Task {
const normalizedTitle = title.trim();
if (normalizedTitle.length < 3) {
throw new BadRequestException('O título deve possuir ao menos 3 caracteres.');
}
return this.repository.add(normalizedTitle);
}
}
O @Inject(TASK_REPOSITORY) é necessário porque o tipo da interface não existe em
ambiente de execução. O token existe.
15. Controller fino
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
list(): Task[] {
return this.tasksService.list();
}
@Post()
create(@Body('title') title: string): Task {
return this.tasksService.create(title);
}
}
Na Aula 5.4 substituiremos a leitura direta do corpo por um DTO validado.
16. Registrando o TasksModule
@Module({
controllers: [TasksController],
providers: [
TasksService,
{
provide: TASK_REPOSITORY,
useClass: InMemoryTasksRepository,
},
],
exports: [TasksService],
})
export class TasksModule {}
O TasksController solicita TasksService. O service solicita TASK_REPOSITORY.
O contêiner resolve o grafo de baixo para cima.
17. Importando no módulo raiz
@Module({
imports: [HealthModule, TasksModule],
})
export class AppModule {}
Com o prefixo global /api, as rotas ficam:
GET /api/health
GET /api/tasks
POST /api/tasks
18. Encapsulamento entre módulos
Suponha que ReportsModule precise de TasksService:
TasksModuleprecisa exportarTasksService;ReportsModuleprecisa importarTasksModule;- só então o service pode ser injetado no módulo consumidor.
@Module({
imports: [TasksModule],
providers: [ReportsService],
})
export class ReportsModule {}
exports não cria uma resposta HTTP nem exporta arquivo JavaScript. Ele controla a
visibilidade do provider no grafo NestJS.
19. Providers globais
@Global() pode tornar providers disponíveis em toda a aplicação, mas o uso excessivo
esconde dependências. Prefira imports explícitos. Global costuma fazer sentido para
infraestrutura transversal cuidadosamente controlada, como configuração.
20. Escopo e tempo de vida
| Escopo | Instância |
|---|---|
DEFAULT |
uma instância compartilhada; padrão recomendado |
REQUEST |
nova instância para cada requisição |
TRANSIENT |
nova instância para cada consumidor |
Não use escopo por requisição apenas porque existe uma requisição HTTP. Ele tem custo adicional e pode propagar o escopo pela cadeia de dependências.
21. Provider ausente
Se TasksService solicitar TASK_REPOSITORY, mas o módulo não registrar esse token,
a inicialização falhará. A mensagem normalmente informa que o NestJS não consegue
resolver uma dependência em determinada posição do construtor.
Lista de verificação de diagnóstico:
- o provider possui decorator quando necessário?
- foi adicionado a
providers? - o token registrado é exatamente o token injetado?
- se vem de outro módulo, foi exportado?
- o módulo consumidor importou o módulo fornecedor?
22. Dependência circular
Uma dependência circular aparece quando A depende de B e B depende de A:
TasksService → ReportsService → TasksService
O NestJS possui forwardRef() para casos inevitáveis, mas a primeira ação deve ser
reavaliar responsabilidades. Muitas dependências circulares indicam limites mal
definidos.
23. Decorators e metadados
Decorators como @Module(), @Controller(), @Get() e @Injectable() não são
comentários. Eles associam metadados às classes e métodos. Durante a inicialização, o
NestJS usa esses metadados para construir módulos, registrar rotas e resolver
dependências.
24. Comparação com Angular
Angular e NestJS usam conceitos parecidos de componentes, decorators e DI, mas têm responsabilidades diferentes:
| Angular | NestJS |
|---|---|
| executa a interface no navegador | executa a API no Node.js |
| componente recebe interação visual | controller recebe HTTP |
| service atende a interface | provider executa regras e infraestrutura |
| router troca telas | router seleciona endpoints |
Eles podem compartilhar ideias arquiteturais, mas não são a mesma aplicação.
25. E o XAMPP?
O Apache do XAMPP continua servindo este curso e pode servir arquivos do Angular. A API NestJS real é outro processo:
Apache/XAMPP: http://localhost/site/PosIA
NestJS: http://localhost:3000/api/tasks
MySQL: serviço de banco, sem acesso direto pelo navegador
Usar XAMPP não obriga a escrever a API em PHP. Apenas tome cuidado para não escolher uma porta já ocupada.
26. Testabilidade
Com dependências injetadas, um teste pode trocar o repositório real por um objeto controlado:
const repository: TasksRepository = {
findAll: () => [],
add: (title) => ({ id: 'test-1', title, completed: false }),
};
const service = new TasksService(repository);
Não há MySQL, rede ou servidor HTTP nesse teste. A regra é verificada isoladamente.
27. Roteiro de implementação
| Etapa | Tempo sugerido |
|---|---|
| Introdução e responsabilidades | 15 min |
| Módulos e encapsulamento | 20 min |
| Controllers e providers | 20 min |
| DI, tokens e repositório | 30 min |
| Escopos e erros comuns | 15 min |
| Laboratório | 15 min |
| Revisão | 5 min |
28. Executando o exemplo real
Na pasta do laboratório:
npm install
npm run start:dev
Depois teste:
curl http://localhost:3000/api/tasks
O laboratório visual não executa esses comandos. Ele simula o contêiner para ser usado com segurança no navegador.
29. Laboratório guiado
Abra o Mapa do contêiner NestJS.
Etapa 1 — Resolva o grafo correto
- mantenha “Registro correto”;
- selecione o escopo
DEFAULT; - clique em Executar inicialização;
- observe a ordem repositório → service → controller.
Etapa 2 — Faça requisições
- liste as tarefas;
- crie uma tarefa válida;
- liste novamente;
- observe que a instância default preserva os dados em memória.
Etapa 3 — Quebre o registro
- selecione “Token do repositório ausente”;
- execute a inicialização;
- identifique exatamente qual dependência não pôde ser resolvida.
Etapa 4 — Compare escopos
- volte ao registro correto;
- compare
DEFAULT,REQUESTeTRANSIENT; - envie duas requisições;
- acompanhe a quantidade simulada de instâncias.
30. Erros comuns
Criar dependências manualmente
const service = new TasksService(new InMemoryTasksRepository());
Isso contorna o contêiner e aumenta o acoplamento.
Colocar regra no controller
Controllers grandes ficam difíceis de testar e reaproveitar.
Confundir interface com token
A interface ajuda o TypeScript, mas desaparece em ambiente de execução.
Exportar tudo
Transforma detalhes internos em dependências públicas.
Usar módulo global para evitar imports
Reduz a visibilidade das relações e dificulta manutenção.
Resolver arquitetura circular somente com forwardRef()
O código pode iniciar, mas o problema de responsabilidades continua.
31. Exercício de fixação
Crie um DocumentsModule contendo:
DocumentsController;DocumentsService;- contrato
DocumentsRepository; - token
DOCUMENTS_REPOSITORY; - implementação em memória;
- rota
GET /api/documents.
Explique por escrito qual elemento seria substituído ao conectar MySQL.
32. Desafio individual
Crie um ReportsModule que consuma uma versão pública do TasksService:
- exporte somente o provider necessário;
- importe
TasksModuleemReportsModule; - gere um resumo com total de tarefas;
- não importe arquivos internos do repositório;
- desenhe o grafo final de módulos e providers.
33. Lista de verificação de conclusão
- Sei explicar por que módulo não é apenas pasta.
- Diferencio
importTypeScript deimportsdo NestJS. - Meu controller apenas traduz HTTP e delega regras.
- Registrei controller e providers no módulo correto.
- Sei por que uma interface não funciona sozinha como token.
- Consigo usar um
Symbolcomo token. - Entendo
imports,providers,controllerseexports. - Reconheço provider ausente e dependência circular.
- Sei comparar os três escopos.
- Mantive o navegador sem acesso direto ao MySQL.
34. Rubrica da entrega
| Critério | Pontos |
|---|---|
| Organização do feature module | 20 |
| Controller fino | 15 |
| Regra no service | 15 |
| Contrato e token do repositório | 15 |
| Registro correto dos providers | 15 |
| Encapsulamento e exports | 10 |
| Explicação do grafo | 10 |
| Total | 100 |
35. Fontes oficiais
- NestJS — Modules
- NestJS — Controllers
- NestJS — Providers
- NestJS — Custom providers
- NestJS — Injection scopes
- NestJS — Circular dependency
Próxima aula
Na Aula 5.3, transformaremos o módulo de tarefas em uma API REST completa, estudando recursos, verbos, parâmetros, status HTTP, idempotência e contratos de resposta.