Dependency Injection
- NestJS
- Node.js
A design pattern where an object receives its collaborators from outside rather than creating them itself, making code looser-coupled and far easier to test.
Prerequisites
- Services and how they use collaborators (see Related Topics)
What you'll understand
- What dependency injection is and the problem it solves
- Why injected dependencies make code easier to test
- What a DI container does for you in a framework
Explanation
Dependency injection (DI) is a simple idea with a big payoff: instead of an object creating the things it depends on, it receives them from outside, usually through its constructor. A service that needs a database client does not call new DatabaseClient() itself; it is handed one. The object declares what it needs and lets someone else decide what to provide.
This inverts control and loosens coupling. When a class builds its own dependencies, it is welded to those exact implementations. When dependencies are injected, you can supply a real database in production and a fake one in a test, without changing the class at all. That substitution is what makes injected code so much easier to test in isolation.
In small programs you inject dependencies by hand, passing them into constructors as you build objects. As an application grows, a DI container automates this wiring: you register what each dependency is, and the container constructs objects with the right collaborators for you. Frameworks like NestJS have a container built in, which is why you can simply list a service in a constructor and receive a ready-made instance.
Examples
This TypeScript example contrasts a hard-wired dependency with an injected one:
// Hard-wired: difficult to test, locked to one implementation
class OrderService {
private db = new PostgresClient();
}
// Injected: the collaborator is supplied from outside
class OrderService {
constructor(private readonly db: DatabaseClient) {}
}A test can now pass a fake DatabaseClient, and a framework container can supply the real one automatically:
const service = new OrderService(new FakeDatabaseClient());
// ...assert on behaviour without a real databaseCommon mistakes
- Creating dependencies with new inside a class, which defeats the purpose of DI.
- Injecting concrete implementations everywhere instead of depending on an interface or abstraction.
- Over-engineering tiny scripts with a container they do not need.
- Hiding dependencies in global state instead of declaring them explicitly.
Best practices
- Declare dependencies explicitly, typically as constructor parameters.
- Depend on abstractions so implementations can be swapped, including in tests.
- Let a container handle wiring once the object graph grows.
- Keep construction (wiring) separate from behaviour (logic).
Further reading
- Martin Fowler, Inversion of Control Containers and the Dependency Injection pattern — https://martinfowler.com/articles/injection.html
- NestJS, Custom providers — https://docs.nestjs.com/fundamentals/custom-providers