Dependency Injection Patterns
Dependency injection (DI) supplies use cases with their dependencies (repositories, gateways, loggers) instead of importing singletons. In Node.js you can wire manually, use Awilix, or rely on NestJS 11 built-in DI.
Search across all documentation pages
Dependency injection (DI) supplies use cases with their dependencies (repositories, gateways, loggers) instead of importing singletons. In Node.js you can wire manually, use Awilix, or rely on NestJS 11 built-in DI.
Quick-reference recipe card - copy-paste ready.
// Manual DI - composition root
const orders = new PostgresOrderRepository(pool);
const createOrder = new CreateOrder(orders);
app.post("/orders", (req, res) => createOrder.execute(req.body));// Awilix - container
import { createContainer, asClass, asValue } from "awilix";
const container = createContainer();
container.register({
pool: asValue(pool),
orderRepository: asClass(PostgresOrderRepository).singleton(),
createOrder: asClass(CreateOrder).singleton(),
});When to reach for this:
// application/create-order.ts
import type { OrderRepository } from "../domain/ports/order-repository";
export class CreateOrder {
constructor(private readonly orders: OrderRepository) {}
async execute(input: { customerId: string; sku: string; qty: number }) {
const order = { id: crypto.randomUUID(), ...input, status: "pending" as const };
await this.orders.save(order);
return order;
}
}
// manual composition root
import express from "express";
import { Pool } from "pg";
import { PostgresOrderRepository } from "./infrastructure/postgres-order-repository";
import { CreateOrder } from "./application/create-order";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const createOrder = new CreateOrder(new PostgresOrderRepository(pool));
const app = express();
app.use(express.json());
app.post("/orders", async (req, res) => {
res.status(201).json({ data: await createOrder.execute(req.body) });
});
// Awilix alternative - src/container.ts
import { createContainer, asClass, asValue, InjectionMode } from "awilix";
export function buildContainer(pool: Pool) {
const container = createContainer({ injectionMode: InjectionMode.CLASSIC });
container.register({
pool: asValue(pool),
orderRepository: asClass(PostgresOrderRepository).singleton(),
createOrder: asClass(CreateOrder).singleton(),
});
return container;
}
// Resolve in main
// const { createOrder } = buildContainer(pool).cradle;What this demonstrates:
CreateOrder depends on OrderRepository interface onlymain.ts or container.ts - the only place that news concrete adapters@Injectable() providers, constructor injection, module imports/exports| Pattern | Pros | Cons | Best for |
|---|---|---|---|
| Manual | Obvious, zero magic, fast tests | Verbose at scale | <15 bindings |
| Awilix | Auto-wiring, scoped lifetimes | Learning curve, runtime resolve | Express/Fastify midsize |
| Nest DI | First-class modules, testing utils | Decorator opinion, bootstrap cost | Nest-standard teams |
proxyquire/module mocks | Quick hack | Brittle, breaks encapsulation | Avoid in production code |
@Injectable()
export class CreateOrder {
constructor(private readonly orders: OrderRepository) {}
}
@Module({
providers: [
CreateOrder,
{ provide: "OrderRepository", useClass: PostgresOrderRepository },
],
controllers: [OrdersController],
})
export class OrdersModule {}// Token-based injection without string magic - use symbols or interfaces
export const OrderRepositoryToken = Symbol("OrderRepository");
type OrderRepository = import("../domain/ports/order-repository").OrderRepository;container.resolve scattered in routes. Fix: Resolve once at boot; inject into handlers.export const db = new Pool(). Fix: Register in composition root; pass to repos.| Alternative | Use When | Don't Use When |
|---|---|---|
| Manual factory functions | Small Fastify services | 30+ use cases with shared deps |
tsyringe | Decorator DI without Nest | Team avoids decorators |
| Pure functions + deps param | Functional style | Large graphs with shared config |
| No DI - import singletons | Throwaway scripts | Long-lived APIs |
When you have 10+ classes to wire and repeated new chains in main.ts. Before that, manual is clearer.
No first-class DI. Use manual, Awilix, or fastify-decorators with care.
Register test doubles: container.register({ orderRepository: asValue(inMemoryRepo) }).
Use AsyncLocalStorage or per-request container child scope in Awilix (scopePerRequest plugin pattern).
REQUEST-scoped providers add overhead. Default singleton; use REQUEST only for true per-request state.
Yes during migration. New modules register in Awilix; legacy stays manual until touched.
TypeScript interfaces erase at runtime. Use abstract class, token symbol, or string token in Nest.
Build a smaller container in worker main with job-specific deps - share pool singleton.
For value objects and pure domain types yes. For repositories and HTTP clients, no - inject them.
Lambda handler top-level: cold start builds container once, reuse across invocations in same instance.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 19, 2026