Engenharia de IA Aplicada à Programação Web
Aula 2 de 8
Navegar por módulos e aulas
  1. 01 Fundamentos da Web
  2. 02 JavaScript moderno
  3. 03 TypeScript, Git e qualidade
  4. 04 Angular
  5. 05 Node.js, NestJS e APIs
  1. 5.1 Introdução ao Node.js e NestJS
  2. 5.2 Módulos, controladores e provedores
  3. 5.3 REST, rotas e respostas HTTP
  4. 5.4 DTOs, pipes e validação
  5. 5.5 Erros, filtros e configuração
  6. 5.6 OpenAPI e documentação
  7. 5.7 Registros, interceptadores e testes
  8. 5.8 Compilação e projeto final

Módulo 5 · Aula 5.2

Aula 5.2 — Módulos, controladores e provedores

2 horas Teoria + laboratório Node.js, NestJS e APIs
Ver fonte Markdown

Identificação

  • Módulo: 5 — Node.js, NestJS e APIs
  • Duração: 2 horas
  • Tipo: teoria aplicada e laboratório
  • Entrega: TasksModule organizado 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?
  • imports do TypeScript é igual ao imports do @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:

  1. explicar módulo, controlador, provedor e injeção de dependência;
  2. diferenciar importação de arquivo e importação de módulo NestJS;
  3. organizar uma capacidade de negócio em um feature module;
  4. manter regras de negócio fora do controller;
  5. registrar e resolver providers pelo contêiner de IoC;
  6. usar classe, string ou Symbol como token de injeção;
  7. explicar encapsulamento, imports e exports;
  8. reconhecer provider ausente e dependência circular;
  9. comparar os escopos default, requisição e transient;
  10. 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:

  1. TasksModule precisa exportar TasksService;
  2. ReportsModule precisa importar TasksModule;
  3. 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:

  1. o provider possui decorator quando necessário?
  2. foi adicionado a providers?
  3. o token registrado é exatamente o token injetado?
  4. se vem de outro módulo, foi exportado?
  5. 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

  1. mantenha “Registro correto”;
  2. selecione o escopo DEFAULT;
  3. clique em Executar inicialização;
  4. observe a ordem repositório → service → controller.

Etapa 2 — Faça requisições

  1. liste as tarefas;
  2. crie uma tarefa válida;
  3. liste novamente;
  4. observe que a instância default preserva os dados em memória.

Etapa 3 — Quebre o registro

  1. selecione “Token do repositório ausente”;
  2. execute a inicialização;
  3. identifique exatamente qual dependência não pôde ser resolvida.

Etapa 4 — Compare escopos

  1. volte ao registro correto;
  2. compare DEFAULT, REQUEST e TRANSIENT;
  3. envie duas requisições;
  4. 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:

  1. exporte somente o provider necessário;
  2. importe TasksModule em ReportsModule;
  3. gere um resumo com total de tarefas;
  4. não importe arquivos internos do repositório;
  5. 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 import TypeScript de imports do 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 Symbol como token.
  • Entendo imports, providers, controllers e exports.
  • 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

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.