Engenharia de IA Aplicada à Programação Web
Aula 4 de 10
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. 4.1 Introdução ao Angular e componentes
  2. 4.2 Templates, bindings e control flow
  3. 4.3 Composição, inputs e outputs
  4. 4.4 Serviços e injeção de dependência
  5. 4.5 Rotas, layouts e navegação
  6. 4.6 Formulários e validação
  7. 4.7 HTTP, APIs e RxJS
  8. 4.8 Signals e estado da interface
  9. 4.9 Interceptors, erros e acessibilidade
  10. 4.10 Testes, compilação e projeto final

Módulo 4 · Aula 4.4

Serviços e injeção de dependência

2 horas Teoria + laboratório Angular
Ver fonte Markdown

Identificação

Introdução

O que é um serviço?

Serviço é uma classe criada para reunir código que não pertence exclusivamente à apresentação de um componente. Ele pode guardar estado compartilhado, aplicar regras de negócio, acessar uma API, registrar eventos ou coordenar outras dependências.

Componente: apresenta a interface e recebe interações
Serviço: mantém dados e executa operações reutilizáveis

Na Knowledge AI, TaskList não precisa conhecer todos os detalhes de armazenamento. Ela pede tarefas ao TaskStore e solicita operações como alternar status.

Serviço é uma API ou servidor?

Não necessariamente. Nesta aula, o serviço é uma classe TypeScript executada no front-end, dentro da aplicação Angular no navegador. Mais adiante, esse serviço usará o cliente HTTP para conversar com a API NestJS.

agora:       componente → serviço Angular → estado em memória
mais adiante: componente → serviço Angular → HTTP → API NestJS → MySQL

O serviço Angular não se conecta diretamente ao MySQL e não guarda credenciais do banco no navegador.

Serviço é igual a função utilitária?

Não. Uma função pura, como formatDate(value), pode ser apenas uma função exportada. Um serviço é útil quando há estado, dependências, ciclo de vida, substituição de implementação ou uso compartilhado pelo sistema de injeção.

O que é uma dependência?

Dependência é algo de que uma classe precisa para realizar seu trabalho. Se TaskList usa TaskStore, então TaskStore é uma dependência de TaskList.

export class TaskList {
  protected readonly taskStore = inject(TaskStore);
}

A classe declara do que precisa, sem decidir como criar a instância.

O que é injeção de dependência?

Injeção de dependência, ou DI, é o mecanismo que entrega uma dependência pronta a quem a solicita. O Angular mantém injetores que procuram um provider para o token pedido e devolvem a instância correspondente.

componente pede TaskStore
        ↓
injector procura provider para TaskStore
        ↓
provider entrega a instância

O que é um injector?

Injector é o objeto do Angular responsável por resolver pedidos. Podemos imaginá-lo como um catálogo: a chave é um token e o valor descreve o que deve ser entregue.

token TaskStore → instância de TaskStore

O token pode ser uma classe ou um InjectionToken. Nesta aula, a própria classe TaskStore será o token.

O que é um provider?

Provider é a configuração que ensina ao injector como obter uma dependência. Para um serviço comum, o provider pode ser registrado automaticamente na raiz. Em casos específicos, ele pode ser registrado manualmente em uma aplicação, rota ou componente.

Por que não usar new TaskStore()?

Com new, o componente escolhe e cria sua dependência. Isso pode gerar uma instância diferente em cada lugar, dificulta substituições em testes e ignora o ciclo de vida controlado pelo Angular.

// Evite dentro do componente
const store = new TaskStore();

// Solicite ao Angular
const store = inject(TaskStore);

Não é que new seja proibido em JavaScript. Ele apenas não é a forma correta de obter uma dependência gerenciada pelo injector.

O que são @Service() e @Injectable()?

Na documentação do Angular 22, @Service() é a forma moderna e concisa de declarar um serviço singleton de raiz:

@Service()
export class TaskStore {}

Em Angular 21 ou anterior, e em muitos projetos atuais, você verá:

@Injectable({ providedIn: "root" })
export class TaskStore {}

As duas formas permitem inject(). @Injectable continua necessário para configurações avançadas e compatibilidade. Este curso mostrará a API atual sem esconder o padrão encontrado em projetos existentes.

O que significa providedIn: "root"?

Significa que o serviço é fornecido pelo injector raiz da aplicação. Normalmente, isso resulta em uma instância compartilhada por todas as partes que usam aquele provider. O serviço também pode ser removido da compilação se não for usado.

Singleton significa uma única instância para sempre?

Somente dentro do escopo daquele provider. Um provider raiz costuma entregar uma instância para a aplicação. Se o mesmo serviço for fornecido novamente em um componente, aquela parte da árvore recebe outra instância, vinculada ao ciclo de vida do componente.

Como isso entra na Knowledge AI?

Moveremos o array de tarefas e suas operações para TaskStore. Dois componentes que injetarem o mesmo serviço raiz observarão o mesmo estado. A interface fica focada em apresentar dados e encaminhar intenções.

Objetivos

Ao final da aula, o aluno deverá conseguir:

  1. explicar serviço, dependência, injector, provider e token;
  2. diferenciar serviço Angular, servidor e função utilitária;
  3. declarar um serviço atual com @Service();
  4. reconhecer @Injectable({ providedIn: "root" });
  5. solicitar uma dependência com inject();
  6. explicar por que evitar new para serviços gerenciados;
  7. mover estado e operações de um componente para um serviço;
  8. expor estado somente para leitura;
  9. diferenciar provider raiz e provider de componente;
  10. reconhecer erros comuns de resolução de dependências.

Pré-requisitos

  • Aulas 4.1 a 4.3 concluídas;
  • classes TypeScript e modificadores de acesso;
  • componentes, signals e estado derivado;
  • atualização imutável de arrays;
  • composição com inputs e outputs.

Pergunta orientadora

Como compartilhar regras e estado sem acoplar cada componente à criação das suas dependências?

Roteiro sugerido

Etapa Duração
Serviço e responsabilidade 20 min
Dependência, provider e injector 30 min
@Service, @Injectable e inject 25 min
Escopos e estado compartilhado 25 min
Laboratório 15 min
Revisão 5 min

1. Problema: componente com responsabilidades demais

export class TaskList {
  readonly tasks = signal<Task[]>(INITIAL_TASKS);

  toggleTask(id: number): void {
    // regra de atualização misturada à interface
  }
}

Isso funciona em uma tela pequena. Quando dashboard, lista e detalhe precisam das mesmas tarefas, duplicar o estado cria divergências.

2. Extraindo o estado

import { computed, Service, signal } from "@angular/core";

@Service()
export class TaskStore {
  private readonly taskState = signal<readonly Task[]>(INITIAL_TASKS);

  readonly tasks = this.taskState.asReadonly();
  readonly openCount = computed(() =>
    this.tasks().filter((task) => task.status !== "done").length,
  );
}

O signal gravável fica privado. Consumidores recebem apenas a leitura e operações nomeadas.

3. Serviço não precisa terminar em Service

O nome deve descrever o papel. TaskStore comunica que a classe mantém estado de tarefas. TaskApi, TaskRepository ou TaskLogger teriam responsabilidades diferentes. Evite um genérico TaskService que acumule tudo.

4. Criando pelo Angular CLI

ng generate service tasks/task-store

O CLI cria um arquivo próprio. A convenção atual usa nomes concisos; projetos anteriores podem gerar task-store.service.ts.

5. @Service() no Angular atual

import { Service } from "@angular/core";

@Service()
export class TaskStore {}

No Angular 22, essa forma registra automaticamente um singleton no injector raiz e suporta dependências obtidas com inject().

6. Forma compatível com versões anteriores

import { Injectable } from "@angular/core";

@Injectable({ providedIn: "root" })
export class TaskStore {}

Use esta forma se a versão instalada ainda não oferece @Service ou se o projeto mantém essa convenção. Não troque toda uma base apenas por estética.

7. Quando manter @Injectable

@Injectable permanece adequado quando há:

  • injeção por parâmetros do construtor;
  • escopo diferente de raiz, como platform;
  • configurações avançadas de provider;
  • necessidade de compatibilidade com Angular anterior;
  • convenção consolidada no projeto.

8. Solicitando com inject()

import { Component, inject } from "@angular/core";
import { TaskStore } from "./task-store";

@Component({ /* ... */ })
export class TaskList {
  protected readonly taskStore = inject(TaskStore);
}

O guia de estilo atual prefere inject() pela legibilidade e inferência de tipos.

9. O que inject(TaskStore) faz?

  1. usa TaskStore como token;
  2. começa a busca no contexto de injeção atual;
  3. procura um provider na hierarquia;
  4. cria a instância quando necessário;
  5. reutiliza a instância dentro do mesmo escopo;
  6. devolve o objeto tipado.

10. Contexto de injeção

inject() não pode ser chamado em qualquer momento e lugar. Ele funciona em um contexto onde o Angular conhece o injector atual, como inicializadores de campos de componentes e serviços ou factories de providers.

export class TaskList {
  private readonly store = inject(TaskStore); // válido
}

Chamá-lo mais tarde em um clique comum pode gerar erro de contexto.

11. Injeção por construtor

constructor(private readonly taskStore: TaskStore) {}

Essa forma continua suportada com @Injectable. É frequente em projetos anteriores. Para código novo, seguiremos inject() conforme o guia de estilo atual.

12. Encapsulando escrita

private readonly taskState = signal<readonly Task[]>(INITIAL_TASKS);
readonly tasks = this.taskState.asReadonly();

Se o signal gravável fosse público, qualquer componente poderia chamar set com um estado inválido. O serviço deve proteger suas invariantes.

13. Operações de domínio

toggle(id: number): void {
  this.taskState.update((tasks) =>
    tasks.map((task) =>
      task.id === id
        ? { ...task, status: task.status === "done" ? "todo" : "done" }
        : task,
    ),
  );
}

O componente diz o que deseja; o serviço controla como o estado muda.

14. Estado derivado no serviço

readonly openCount = computed(() =>
  this.tasks().filter((task) => task.status !== "done").length,
);

Como a regra interessa a diferentes telas, centralizá-la evita resultados divergentes.

15. Consumindo no template

<p>{{ taskStore.openCount() }} tarefas abertas</p>

@for (task of taskStore.tasks(); track task.id) {
  <app-task-card
    [task]="task"
    (toggleRequested)="taskStore.toggle($event)"
  />
}

Para uma aplicação maior, o componente pode expor métodos próprios para evitar acoplar todo o template ao serviço. Aqui deixamos a ligação visível para estudo.

16. Raiz compartilhada

Root EnvironmentInjector
└── TaskStore · instância A
    ├── Dashboard recebe A
    └── TaskList recebe A

Quando o dashboard altera a instância A, a lista lê o mesmo estado reativo.

17. Provider no componente

Com um serviço não fornecido automaticamente, ou sobrescrevendo o provider raiz:

@Component({
  providers: [TaskStore],
})
export class IsolatedTaskPanel {
  private readonly store = inject(TaskStore);
}

Cada instância do componente cria seu próprio escopo e pode receber um TaskStore isolado.

18. Hierarquia de injetores

componente atual
      ↓ se não encontrou
componente ancestral
      ↓ se não encontrou
EnvironmentInjector da aplicação
      ↓ se não encontrou
erro de provider ausente

O Angular usa o primeiro provider encontrado para aquele token.

19. Token e implementação

Na forma simples, a classe cumpre os dois papéis:

providers: [TaskStore]

É um atalho para:

providers: [{ provide: TaskStore, useClass: TaskStore }]

O token é a chave; useClass descreve a implementação.

20. Interfaces não existem em ambiente de execução

Uma interface TypeScript desaparece depois da compilação e não pode ser usada diretamente como token:

interface TaskGateway {}
// inject(TaskGateway) // não funciona

Para contratos sem classe, usamos InjectionToken<TaskGateway>. Esse recurso será aprofundado quando houver implementações reais e simuladas.

21. Provider raiz ou local?

Necessidade Escopo sugerido
estado compartilhado pela aplicação raiz
instância isolada por editor componente
dependência específica de uma área rota
configuração global aplicação ou token raiz

Escolha conscientemente. “Singleton” não deve ser uma consequência acidental.

22. Ciclo de vida

Um serviço raiz costuma viver durante a aplicação. Um serviço fornecido pelo componente é destruído com aquela instância do componente. Isso importa para estado, subscriptions e recursos externos.

23. Serviço não é depósito de qualquer código

Evite uma classe gigante com autenticação, tarefas, notificações e formatação. Separe por responsabilidade e área de domínio. A injeção ajuda a compor serviços menores.

24. Serviço pode depender de outro serviço

@Service()
export class TaskAudit {
  private readonly clock = inject(Clock);
}

O injector resolve a árvore de dependências. Dependências circulares indicam uma fronteira arquitetural que precisa ser revista.

25. Testabilidade

Quando o componente solicita um token, um teste pode fornecer outra implementação. Isso permite isolar a interface sem acessar rede ou banco. Não significa criar mocks para tudo; substitua fronteiras externas ou comportamentos difíceis de controlar.

26. Laboratório guiado

Abra o mapa de serviço e injetores.

Etapa 1 — Use o provider raiz

Altere uma tarefa no painel A. Observe o painel B lendo a mesma instância.

Etapa 2 — Observe a resolução

O registro mostra o pedido, a busca do token e a instância entregue.

Etapa 3 — Ative instâncias locais

Compare dois componentes com providers próprios. Uma alteração deixa de aparecer no outro painel.

Etapa 4 — Examine a fonte

Alterne entre serviço, componente e escopo do provider.

27. Sobre o laboratório estático

O laboratório reproduz o comportamento para abrir sem instalar Angular. A pasta src/app contém a implementação Angular equivalente com @Service(), inject() e signals. A simulação serve para observar identidade e escopo; não implementa o injector real do framework.

28. Erros comuns

Esquecer o provider

Se o injector não encontra o token na hierarquia, ocorre erro de resolução.

Usar new no componente

O Angular perde controle sobre criação, compartilhamento e substituição.

Tornar o signal gravável público

Qualquer consumidor pode quebrar as regras do estado.

Achar que todo serviço é global

O escopo depende de onde o provider foi registrado.

Usar inject() fora de contexto

A função precisa de um contexto de injeção ativo.

Colocar todo código em serviços

Interação visual pertence ao componente; uma transformação pura pode ser uma função.

Confundir serviço Angular com back-end

O código continua no navegador e não deve receber segredos de infraestrutura.

29. Boas práticas

  • escolha nomes que expressem responsabilidade;
  • mantenha um conceito principal por arquivo;
  • prefira inject() em código Angular novo;
  • preserve @Injectable quando a versão ou configuração exigir;
  • mantenha estado gravável privado;
  • exponha leitura e operações explícitas;
  • registre o provider no menor escopo correto;
  • evite dependências circulares;
  • mantenha componentes focados na interface;
  • nunca conecte o front-end diretamente ao MySQL.

30. Exercícios

Exercício 1 — Extração

Mova uma lista de usuários de um componente para UserStore.

Exercício 2 — Leitura protegida

Exponha um signal somente para leitura e uma operação add.

Exercício 3 — Injeção

Use inject(UserStore) em dois componentes.

Exercício 4 — Escopo

Explique a diferença entre provider raiz e providers: [UserStore] no componente.

Exercício 5 — Decisão

Classifique formatação de moeda, estado de sessão e clique de expansão como função, serviço ou responsabilidade do componente.

31. Desafio

Crie um serviço de tarefas que:

  • seja fornecido na raiz;
  • mantenha o signal gravável privado;
  • exponha tarefas somente para leitura;
  • calcule totais com computed;
  • possua operações de adicionar, alternar e remover;
  • seja injetado com inject();
  • seja consumido por dois componentes;
  • demonstre que ambos recebem a mesma instância;
  • não contenha código visual;
  • não acesse MySQL diretamente.

32. Lista de verificação de conclusão

  • Explico o que é um serviço Angular.
  • Diferencio serviço front-end e servidor.
  • Identifico uma dependência.
  • Explico injector, provider e token.
  • Reconheço @Service() e @Injectable().
  • Uso inject() em contexto válido.
  • Evito criar serviços gerenciados com new.
  • Encapsulo a escrita do estado.
  • Diferencio provider raiz e local.
  • Sei que interfaces não servem diretamente como tokens.

33. Critérios de avaliação

Critério Pontos
Separação de responsabilidades 20
Declaração e injeção do serviço 25
Encapsulamento do estado 20
Entendimento de provider e escopo 20
Clareza, tipos e acessibilidade 15
Total 100

Resumo

Nesta aula, aprendemos que:

  • serviço reúne estado ou comportamento reutilizável fora da apresentação;
  • dependência é algo de que uma classe precisa;
  • provider associa um token ao valor entregue;
  • injector resolve pedidos pela hierarquia;
  • @Service() é o atalho moderno para singleton raiz no Angular 22;
  • @Injectable({ providedIn: "root" }) continua válido e compatível;
  • inject() é a forma preferida em código novo;
  • provider raiz compartilha uma instância dentro daquele escopo;
  • provider de componente pode criar instâncias isoladas;
  • escrita privada e operações nomeadas protegem o estado.

Próxima aula

Na Aula 4.5, criaremos rotas, layouts e navegação para transformar os componentes em uma aplicação com múltiplas páginas.

Fontes oficiais