A Injeção de Dependência no Node.js separa a criação dos objetos do código que usa esses objetos. Em vez de um serviço instanciar diretamente banco, logger, cliente HTTP e fila, essas dependências são fornecidas pelo ponto de composição da aplicação.
Essa técnica facilita testes, troca de implementação e isolamento entre domínio e infraestrutura. Porém, um container de injeção não corrige automaticamente um design acoplado. Tokens globais, service locator escondido, dependências opcionais demais e ciclos entre módulos podem tornar o sistema mais difícil de entender.
Neste guia, você aprenderá constructor injection, factories, tokens, composition root, interfaces TypeScript, testes, scopes, containers, service locator, ciclos e boas práticas para aplicações Node.js.
O que é Dependency Injection?
Dependency Injection, ou DI, é um padrão em que um componente recebe suas dependências externas. O artigo clássico Inversion of Control Containers and the Dependency Injection Pattern, de Martin Fowler, discute constructor injection, setter injection e service locator. A documentação de custom providers do NestJS mostra uma aplicação moderna no ecossistema Node.js.
Exemplo acoplado
class CreateOrderService {
constructor() {
this.orders = new PostgresOrdersRepository();
this.payments = new StripePaymentsClient();
this.logger = new PinoLogger();
}
}O serviço conhece implementações concretas, configuração e criação. Um teste unitário precisa lidar com banco e rede ou alterar internamente a classe.
Constructor injection
class CreateOrderService {
constructor({ orders, payments, logger }) {
this.orders = orders;
this.payments = payments;
this.logger = logger;
}
}Agora o serviço depende apenas do contrato usado. O ponto de composição decide quais objetos fornecer.
Composition root
const pool = createPostgresPool(config.database);
const logger = createLogger(config.logging);
const ordersRepository = new PostgresOrdersRepository({
pool
});
const paymentsClient = new StripePaymentsClient({
apiKey: config.stripeApiKey,
timeoutMs: 3000
});
const createOrder = new CreateOrderService({
orders: ordersRepository,
payments: paymentsClient,
logger
});O composition root é o local onde implementações são criadas e conectadas. Normalmente fica próximo ao bootstrap.
Separar configuração de uso
O serviço de domínio não precisa saber de variáveis de ambiente, Secrets ou nomes de tabelas. Ele recebe objetos prontos.
Consulte Gestão de Segredos no Node.js.
Interfaces TypeScript
interface OrdersRepository {
create(input: CreateOrderRecord): Promise<Order>;
findById(input: FindOrderInput): Promise<Order | null>;
}A classe usa a interface em compilação, mas interfaces não existem no runtime. Containers que resolvem dependências precisam de tokens, classes ou metadata.
Structural typing
TypeScript aceita qualquer objeto compatível com a forma da interface. Isso facilita fakes simples:
const ordersRepositoryFake = {
async create(input) {
return {
id: 'order-1',
...input
};
}
};Tokens de runtime
export const ORDERS_REPOSITORY = Symbol(
'ORDERS_REPOSITORY'
);Symbols evitam colisões de string e representam contratos no container.
Factory manual
function createApplication(config) {
const pool = createPool(config.databaseUrl);
const repositories = createRepositories({ pool });
const services = createServices({
repositories,
logger: createLogger(config)
});
return {
services,
close: async () => pool.end()
};
}Para muitos projetos, factories manuais são suficientes e mantêm o wiring explícito.
Container DI
Frameworks e bibliotecas podem registrar providers e resolver grafos automaticamente. Um exemplo conceitual:
container.register(ORDERS_REPOSITORY, {
useClass: PostgresOrdersRepository
});
container.register(CreateOrderService, {
useClass: CreateOrderService
});Containers reduzem boilerplate, mas adicionam um sistema de resolução que a equipe precisa entender.
NestJS
NestJS possui container integrado, modules e providers. Consulte NestJS no Node.js.
Provider por classe
@Injectable()
class UsersService {
constructor(
private readonly repository: UsersRepository
) {}
}Quando a implementação concreta é suficiente, a classe pode ser o token.
Provider por token
{
provide: USER_REPOSITORY,
useClass: PostgresUsersRepository
}Isso permite trocar a implementação sem alterar o consumidor.
useValue
{
provide: CLOCK,
useValue: systemClock
}É adequado para objetos prontos, configuração imutável e fakes.
useFactory
{
provide: PAYMENTS_CLIENT,
inject: [CONFIG, LOGGER],
useFactory: (config, logger) =>
new PaymentsClient({
apiKey: config.paymentsApiKey,
logger
})
}Factories são úteis quando a construção depende de outras dependências.
Constructor injection como padrão
O construtor torna dependências obrigatórias visíveis. Um objeto não pode ser criado em estado incompleto.
Setter injection
service.setLogger(logger);Setter injection pode ser útil para dependência opcional ou ciclo controlado, mas permite objeto temporariamente inválido.
Property injection
Decorators podem injetar propriedades após a construção. Isso esconde dependências da assinatura e dificulta testes. Prefira construtor quando possível.
Service locator
class CreateOrderService {
execute(input) {
const repository = container.resolve(
ORDERS_REPOSITORY
);
}
}O serviço busca dependências em um registry global. Isso parece conveniente, mas esconde requisitos e acopla o domínio ao container.
Por que evitar service locator?
- dependências não aparecem no construtor;
- erros surgem apenas em runtime;
- testes precisam configurar estado global;
- código depende da API do container;
- ordem de configuração pode afetar resultados.
Container somente no composition root
Uma abordagem segura usa o container para montar o grafo e entrega instâncias aos módulos. O domínio não chama resolve().
Dependências obrigatórias
constructor({ repository, clock, idGenerator }) {
if (!repository || !clock || !idGenerator) {
throw new Error('Dependências obrigatórias ausentes');
}
}Em TypeScript, tipos ajudam durante build, mas validações podem proteger JavaScript e configurações dinâmicas.
Dependências opcionais
Muitas opções podem indicar que a classe possui responsabilidades demais. Prefira um objeto Null quando o comportamento opcional é simples.
Null Object
const noOpMetrics = {
increment() {},
observe() {}
};O serviço sempre recebe um objeto com o contrato, sem condicionais espalhadas.
Ports and adapters
O domínio define ports, como repository, payment gateway e event publisher. Infraestrutura fornece adapters.
Repository port
type UserRepository = {
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
};Adapter PostgreSQL
class PostgresUserRepository {
constructor({ pool }) {
this.pool = pool;
}
async findByEmail(email) {
const result = await this.pool.query(
'SELECT id, email FROM users WHERE email = $1',
[email]
);
return result.rows[0] ?? null;
}
}Adapter em memória
class InMemoryUserRepository {
constructor() {
this.users = new Map();
}
}O mesmo serviço pode ser testado sem banco.
Injetando relógio
const systemClock = {
now: () => new Date()
};Testes usam um relógio fixo, evitando depender da hora real.
Injetando gerador de ID
const uuidGenerator = {
next: () => crypto.randomUUID()
};Um fake pode retornar IDs previsíveis nos testes.
Injetando logger
O logger deve ser uma dependência transversal pequena. Consulte Logs com Pino no Node.js.
Injetando contexto?
Não injete request completo no domínio. Extraia identidade, tenant e valores necessários. Contexto HTTP não deve atravessar todas as camadas.
AsyncLocalStorage
Request ID e tracing podem ser obtidos por contexto assíncrono quando adequado. Consulte AsyncLocalStorage no Node.js.
Scopes
- singleton: uma instância para a aplicação;
- request: uma instância por requisição;
- transient: nova instância por resolução.
Singleton
É adequado para serviços stateless, clients e repositories que usam pools compartilhados. Não armazene usuário atual em singleton.
Request scope
É útil quando a instância realmente contém contexto por requisição, mas aumenta criação de objetos e pode propagar scope pelo grafo.
Transient
Use quando a instância possui estado de curta duração e não pode ser compartilhada. Evite como padrão.
Conexão de banco
Injete o pool singleton, não uma conexão global. Transações recebem um client específico pelo escopo do caso de uso.
Consulte Pool PostgreSQL no Node.js e Transações PostgreSQL no Node.js.
Unit of Work
await unitOfWork.run(async transaction => {
const orders = transaction.ordersRepository;
const outbox = transaction.outboxRepository;
await orders.save(order);
await outbox.add(event);
});O escopo transacional fornece repositories ligados à mesma conexão.
Ciclos de dependência
OrdersService -> PaymentsService
PaymentsService -> OrdersServiceO container pode detectar ou contornar o ciclo, mas a arquitetura continua acoplada.
Como quebrar ciclos?
- extrair uma abstração;
- mover coordenação para um terceiro serviço;
- publicar evento;
- rever limites de módulo;
- inverter uma dependência.
Eventos
Um serviço pode publicar PaymentAuthorized em vez de chamar Orders diretamente. Use outbox quando a entrega precisa ser confiável.
Consulte Outbox Pattern no Node.js.
Lazy injection
Resolver uma dependência sob demanda pode esconder ciclos. Use apenas quando o custo de criação é real e a dependência permanece explícita.
Configuração por ambiente
const emailSender = config.email.driver === 'ses'
? new SesEmailSender(config)
: new SmtpEmailSender(config);O domínio recebe emailSender sem conhecer o ambiente.
Feature flags
Não troque todo o grafo a cada request sem necessidade. Um adapter pode consultar a flag e encaminhar para implementações controladas.
Decorators e metadata
Containers baseados em decorators podem inferir classes, mas interfaces não existem no runtime. Tokens explícitos tornam contratos mais claros.
Minificação e nomes de classe
Não use nomes de parâmetros ou classes como chaves frágeis quando build e minificação podem alterá-los.
Erros de resolução
Valide o container no startup. Uma dependência ausente não deve aparecer apenas na primeira requisição.
Health check do grafo
Resolver todas as dependências principais durante bootstrap detecta configuração incompleta, mas não precisa executar chamadas externas.
Inicialização assíncrona
Clients podem precisar carregar certificados ou configuração. Prefira uma factory async no bootstrap e entregue o objeto pronto.
Não fazer I/O no construtor
Construtores síncronos devem apenas atribuir e validar dependências. Conexões e chamadas externas em construtor dificultam erro, retry e teste.
Lifecycle
O composition root também precisa fechar recursos:
async function closeApplication() {
await queue.close();
await pool.end();
await server.close();
}Consulte Graceful Shutdown no Node.js.
Teste unitário
test('cria pedido e publica evento', async () => {
const orders = new InMemoryOrdersRepository();
const publisher = new FakeEventPublisher();
const clock = { now: () => fixedDate };
const service = new CreateOrderService({
orders,
publisher,
clock,
idGenerator: { next: () => 'order-1' }
});
const order = await service.execute(input);
assert.equal(order.id, 'order-1');
assert.equal(publisher.events.length, 1);
});Fakes versus mocks
Fakes implementam comportamento simples e podem gerar testes legíveis. Mocks verificam interações específicas. Use o nível necessário.
Não mockar tudo
Repositories SQL, serialização e integração HTTP precisam de testes reais. DI facilita substituição, mas não elimina integração.
Teste do composition root
Crie a aplicação com configuração de teste e confirme que o grafo resolve, o servidor inicia e recursos fecham.
Override em framework
NestJS permite substituir providers no TestingModule. Consulte NestJS no Node.js.
Teste de scope
Confirme que singletons não mantêm usuário e que requests recebem contexto isolado.
Teste de ciclo
Um teste de arquitetura pode proibir imports entre módulos em direções indevidas.
Static analysis
Ferramentas podem validar dependency graph, imports circulares e camadas. Use regras para impedir domínio importando infraestrutura.
Performance
Construção manual costuma ter custo mínimo. Containers com reflection e request scope adicionam overhead; meça em aplicações de alto volume.
Memória
Um singleton que mantém cache sem limite ou listeners pode reter dados. O container não gerencia automaticamente lifecycle de tudo.
Observabilidade
Não crie spans para cada resolução de dependência em produção. Observe casos de uso, banco e rede.
Segurança
Não registre tokens do container nem permita que input do usuário selecione uma implementação arbitrária.
Plugin system
Plugins carregados por configuração precisam de allowlist. Importar caminho controlado pelo usuário pode executar código.
Multi-tenancy
Não crie um container permanente por tenant sem limites. Para diferenças de configuração, injete um resolver ou contexto validado.
Consulte Multi-Tenancy no Node.js.
Erros comuns
- new dentro do serviço: implementação fica acoplada.
- Container global no domínio: service locator esconde dependências.
- Request scope para tudo: custo e complexidade crescem.
- Interfaces sem tokens: resolução falha no runtime.
- forwardRef em excesso: ciclos de arquitetura permanecem.
- I/O no construtor: inicialização fica imprevisível.
- Mockar toda infraestrutura: integração real não é testada.
Boas práticas
- Prefira constructor injection.
- Mantenha composition root explícito.
- Dependa de contratos pequenos.
- Use tokens de runtime.
- Use singleton para serviços stateless.
- Evite service locator.
- Quebre ciclos arquiteturalmente.
- Não faça I/O no construtor.
- Teste o grafo e o shutdown.
- Combine unitários e integração.
Conclusão
A Injeção de Dependência no Node.js torna a criação de objetos uma responsabilidade do bootstrap, permitindo que serviços dependam de contratos em vez de implementações concretas.
Constructor injection e composition root explícito costumam oferecer o melhor equilíbrio entre clareza e flexibilidade. Containers são úteis em grafos grandes, desde que não escondam dependências ou ciclos. Com scopes controlados, adapters e testes reais, DI melhora a arquitetura sem transformar o container em uma variável global invisível.


