Dependency Injection in der Praxis
Dependency Injection braucht weder Framework noch Decorators — so wendet man sie in TypeScript mit einfachen Mustern an, die Code testbar und flexibel machen.

Dependency Injection hat ein Imageproblem. Erwähnt man es, denken Entwickler an Annotation-Suppe im Java-Stil, XML-Konfigurationsdateien und Framework-Magie. In TypeScript ist DI einfach nur das Übergeben von Argumenten an Funktionen. Keine Decorators, keine Container, keine Zeremonie — nur explizite Abhängigkeiten, die Code testbar und komponierbar machen.
Das Problem, das DI löst
Wenn ein Modul seine eigenen Abhängigkeiten erzeugt, wird es unmöglich, es isoliert zu testen oder Implementierungen auszutauschen.
// ❌ 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)Die Funktion weiß nicht und kümmert sich nicht darum, ob sie mit Prisma, einem Mock oder einem In-Memory-Array spricht. Das ist das gesamte Wertversprechen.
Drei Muster in TypeScript
Constructor Injection (Klassen)
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;
}
}Factory-Function-Injection (funktionaler Stil)
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;
},
};
}Parameter Injection (am einfachsten)
async function placeOrder(
input: PlaceOrderInput,
orderRepo: OrderRepository,
paymentGateway: PaymentGateway,
) {
const order = await orderRepo.create(input);
await paymentGateway.charge(order.total, input.paymentMethod);
return order;
}Factory-Funktionen sind der Sweet Spot für die meisten TypeScript-Projekte. Sie geben dir Closures über Abhängigkeiten ohne Klassen-Zeremonie, und sie lassen sich natürlich komponieren.
Abhängigkeiten am Rand verdrahten
Der Composition Root ist der Ort, an dem echte Implementierungen erzeugt und miteinander verdrahtet werden. Er liegt am Einstiegspunkt der Anwendung — einem Server-Bootstrap, einer CLI-Main-Funktion oder einem Test-Setup.
// 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);Route-Handler importieren aus dem Composition Root. Services importieren niemals konkrete Implementierungen direkt.
Testen wird trivial
Mit DI kann jeder Test jede Abhängigkeit ersetzen. Keine Mocking-Bibliotheken, kein Monkey-Patching, keine jest.mock()-Magie.
// ❌ 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);
});Der Test ist schnell, deterministisch und liest sich wie eine Spezifikation. Keine Datenbank, kein Netzwerk, kein Dateisystem.
Wann man keinen DI-Container braucht
DI-Container (wie tsyringe, inversify oder awilix) fügen automatische Auflösung hinzu — man registriert Interfaces und Implementierungen, und der Container verdrahtet sie. Das ist nützlich, wenn man 50+ Services mit tiefen Abhängigkeitsgraphen hat.
Für die meisten Webanwendungen ist manuelles Verdrahten in einem Composition Root klarer und leichter zu debuggen:
| Ansatz | Geeignet für | Nachteil |
|---|---|---|
| Manuelles Verdrahten | Kleine bis mittlere Apps, ≤20 Services | Umständlich bei tiefem Abhängigkeitsgraphen |
| DI-Container | Große Apps, Plugin-Systeme, tiefe Graphen | Magische Auflösung, schwerer nachzuvollziehen |
Wenn dein Composition Root in eine Datei passt und du ihn von oben nach unten lesen kannst, brauchst du keinen Container.
Die wichtigsten Punkte
- DI ist einfach nur das Übergeben von Argumenten — kein Framework, keine Decorator-Zeremonie nötig
- Factory-Funktionen sind der pragmatische Sweet Spot in TypeScript
- Der Composition Root ist der einzige Ort, an dem echte Implementierungen verdrahtet werden
- Testen mit DI ist trivial — übergib Fakes direkt, keine Mock-Bibliotheken nötig
- Verzichte auf DI-Container, sofern dein Abhängigkeitsgraph nicht wirklich groß und tief ist


