Inyección de dependencias en la práctica
La inyección de dependencias no exige framework ni decoradores: aplícala en TypeScript con patrones simples que hacen el código testeable y flexible.

La inyección de dependencias tiene un problema de imagen. Mencionala y los desarrolladores se imaginan una sopa de anotaciones al estilo Java, archivos de configuración XML y magia de framework. En TypeScript, DI es simplemente pasar argumentos a funciones. Sin decoradores, sin contenedores, sin ceremonia — solo dependencias explícitas que hacen el código testeable y componible.
El problema que resuelve DI
Cuando un módulo crea sus propias dependencias, se vuelve imposible testearlo de forma aislada o intercambiar implementaciones.
// ❌ Hard-coded dependency — impossible to test without a real database
import { prisma } from "./db";
export async function getActiveUsers() {
return prisma.user.findMany({
where: { status: "active" },
});
}
// How do you test this without a running database?
// How do you switch to a different ORM?
// How do you mock slow queries in development?// ✅ Dependency is injected — caller decides what to provide
interface UserRepository {
findMany(filter: { where: { status: string } }): Promise<User[]>;
}
export function createUserService(repo: UserRepository) {
return {
async getActiveUsers() {
return repo.findMany({ where: { status: "active" } });
},
};
}
// Production: createUserService(prisma.user)
// Tests: createUserService(mockUserRepo)La función no sabe ni le importa si está hablando con Prisma, un mock o un array en memoria. Esa es toda la propuesta de valor.
Tres patrones en TypeScript
Inyección por constructor (clases)
class OrderService {
constructor(
private readonly orderRepo: OrderRepository,
private readonly paymentGateway: PaymentGateway,
private readonly emailService: EmailService,
) {}
async placeOrder(input: PlaceOrderInput): Promise<Order> {
const order = await this.orderRepo.create(input);
await this.paymentGateway.charge(order.total, input.paymentMethod);
await this.emailService.sendConfirmation(order);
return order;
}
}Inyección mediante funciones factory (estilo funcional)
function createOrderService(deps: {
orderRepo: OrderRepository;
paymentGateway: PaymentGateway;
emailService: EmailService;
}) {
return {
async placeOrder(input: PlaceOrderInput): Promise<Order> {
const order = await deps.orderRepo.create(input);
await deps.paymentGateway.charge(order.total, input.paymentMethod);
await deps.emailService.sendConfirmation(order);
return order;
},
};
}Inyección por parámetros (la más simple)
async function placeOrder(
input: PlaceOrderInput,
orderRepo: OrderRepository,
paymentGateway: PaymentGateway,
) {
const order = await orderRepo.create(input);
await paymentGateway.charge(order.total, input.paymentMethod);
return order;
}Las funciones factory son el punto óptimo para la mayoría de los proyectos TypeScript. Te dan closures sobre las dependencias sin la ceremonia de las clases, y componen de forma natural.
Conectar las dependencias en el borde
El composition root es donde creas las implementaciones reales y las conectas entre sí. Vive en el punto de entrada de la aplicación — el bootstrap del servidor, la función main de una CLI, o el setup de tests.
// src/composition-root.ts — the ONE place where real deps are created
import { PrismaClient } from "@prisma/client";
import { StripeGateway } from "./infra/stripe";
import { SendGridEmailService } from "./infra/sendgrid";
import { createOrderService } from "./services/order";
import { createUserService } from "./services/user";
const prisma = new PrismaClient();
const paymentGateway = new StripeGateway(process.env.STRIPE_KEY!);
const emailService = new SendGridEmailService(process.env.SENDGRID_KEY!);
export const orderService = createOrderService({
orderRepo: prisma.order,
paymentGateway,
emailService,
});
export const userService = createUserService(prisma.user);Los route handlers importan desde el composition root. Los servicios nunca importan implementaciones concretas directamente.
Testear se vuelve trivial
Con DI, cada test puede sustituir cualquier dependencia. Sin librerías de mocking, sin monkey-patching, sin la magia de jest.mock().
// ❌ Without DI — need complex mocking setup
jest.mock("./db", () => ({
prisma: { user: { findMany: jest.fn() } },
}));
// ✅ With DI — just pass a fake
import { createUserService } from "./services/user";
test("getActiveUsers filters by status", async () => {
const mockRepo = {
findMany: async (filter: any) => {
expect(filter.where.status).toBe("active");
return [
{ id: "1", name: "Alice", status: "active" },
{ id: "2", name: "Bob", status: "active" },
];
},
};
const service = createUserService(mockRepo);
const users = await service.getActiveUsers();
expect(users).toHaveLength(2);
});El test es rápido, determinístico y se lee como una especificación. Sin base de datos, sin red, sin filesystem.
Cuándo no necesitas un contenedor de DI
Los contenedores de DI (como tsyringe, inversify o awilix) agregan resolución automática — registras interfaces e implementaciones, y el contenedor las conecta. Esto es útil cuando tienes 50+ servicios con grafos de dependencias profundos.
Para la mayoría de las aplicaciones web, el cableado manual en un composition root es más claro y más fácil de depurar:
| Enfoque | Ideal para | Desventaja |
|---|---|---|
| Cableado manual | Apps pequeñas a medianas, ≤20 servicios | Verboso cuando el grafo de dependencias es profundo |
| Contenedor de DI | Apps grandes, sistemas de plugins, grafos profundos | Resolución mágica, más difícil de rastrear |
Si tu composition root cabe en un solo archivo y puedes leerlo de arriba a abajo, no necesitas un contenedor.
Puntos clave
- DI es simplemente pasar argumentos — no requiere ceremonia de framework ni decoradores
- Las funciones factory son el punto óptimo pragmático en TypeScript
- El composition root es el único lugar donde se conectan las implementaciones reales
- Testear con DI es trivial — pasa fakes directamente, sin necesidad de librerías de mocking
- Evita los contenedores de DI a menos que tu grafo de dependencias sea realmente grande y profundo


