Express.js
Dependency injection pragmática
Explica cómo hacer visibles las dependencias mediante factories y composición manual, facilitando tests sin introducir un contenedor complejo por defecto.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Express.js
Explica cómo hacer visibles las dependencias mediante factories y composición manual, facilitando tests sin introducir un contenedor complejo por defecto.
Dependency injection significa que una pieza recibe lo que necesita en lugar de buscarlo globalmente. Puede implementarse con funciones y objetos simples; no exige un contenedor.
módulo necesita repository, logger y clock
↓
composition root los construye
↓
los entrega explícitamenteEsto hace visibles las dependencias y permite sustituirlas en tests.
Un import global oculta acoplamiento:
import { prisma } from '../../database.js';
import { logger } from '../../logger.js';El caso de uso queda ligado a instancias concretas, inicialización global y estado compartido.
Con inyección:
class CreateOrder {
constructor(
private readonly orders: OrderRepository,
private readonly clock: Clock,
private readonly ids: IdGenerator,
) {}
}Adecuado para objetos con dependencias estables. Obliga a construir una instancia válida desde el inicio.
export function createOrder(deps: Dependencies) {
return async function execute(input: Input) {
// use deps
};
}Es simple y natural en TypeScript. No necesitas clases para cada caso de uso.
export function makeCreateOrderHandler(createOrder: CreateOrder) {
return async (request: Request, response: Response) => {
const result = await createOrder.execute(response.locals.input);
response.status(201).json({ data: result });
};
}El handler no importa la implementación.
La aplicación define el contrato que necesita:
interface PaymentGateway {
createIntent(input: PaymentIntentInput): Promise<PaymentIntent>;
}Infrastructure implementa ese contrato. No copies toda la API del proveedor; expresa operaciones del módulo.
TypeScript permite que un objeto cumpla una interfaz sin implements explícito. Esto facilita fakes pequeños:
const clock: Clock = {
now: () => new Date('2026-07-24T00:00:00Z'),
};Logger, config, pool y clientes thread-safe suelen compartirse.
Actor, tenant, request ID y deadline pertenecen al contexto de la request, no a singletons mutables.
Una conexión transaccional o unit of work vive durante el caso de uso.
No guardes actor actual en una variable global.
const pool = createPool(config.database);
const orders = new PostgresOrderRepository(pool);
const createOrder = new CreateOrder({ orders, clock, ids });
const app = createApp({ createOrder, logger });Este wiring explícito puede ser largo, pero es legible y se ejecuta una vez.
Un contenedor puede ayudar cuando existen muchos módulos, scopes y lifecycle. También introduce:
Adóptalo cuando el beneficio supera la complejidad, no para evitar unas líneas de wiring.
Pasar un contenedor global y resolver dentro:
container.resolve('ordersRepository')oculta dependencias y se parece a globals. Prefiere resolución en composition root.
Evita objetos con muchas opciones ambiguas. Si una feature puede operar sin email, representa la decisión:
NoopEmailNotifier explícita.undefined propagado por todas partes suele complicar.
Logging, tracing y transactions pueden inyectarse o envolverse mediante decorators:
const tracedCreateOrder = withTracing(createOrder, tracer);No fuerces todo a middleware HTTP si también se ejecuta desde jobs.
const orders = new InMemoryOrderRepository();
const payments = new FakePaymentGateway();
const useCase = new CreateOrder({ orders, payments, clock, ids });El test controla comportamiento sin mockear módulos globales.
Aun así, integration tests verifican implementaciones reales.
interface AppServices {
orders: ...;
users: ...;
payments: ...;
emails: ...;
// everything
}Pasar esto a cualquier función vuelve dependencias implícitas. Define contratos pequeños por consumidor.
CancelOrder necesita order repository y clock, no Express, pool ni logger global. El handler recibe el caso de uso; el repository recibe pool. Cada nivel conoce lo necesario.
Las dependencias que abren recursos deben cerrarse en shutdown. Composition root puede devolver:
const runtime = {
app,
close: async () => {
await workers.close();
await pool.end();
},
};Una función pura que suma totales no necesita interface o factory. Inyección aporta valor en efectos, variabilidad, tiempo, IDs y dependencias externas.
Configuración y variables de entorno construye una dependencia validada en lugar de lecturas globales dispersas.