All topics
Patternsadvanced

Dependency Injection in JavaScript

Passing a component's dependencies in from outside rather than having it construct or reach out for them internally, improving testability and flexibility.

Dependency injection (DI) is a design principle where a function, class, or module receives the other pieces of code it depends on (its 'dependencies') as explicit parameters or arguments, rather than constructing those dependencies internally or reaching out to fetch them directly (like importing a specific concrete module or instantiating a specific class by name deep inside its own logic). It's less a JavaScript-specific pattern than a general architectural principle, but it shows up constantly in JS backend frameworks and is a favorite senior-level interview topic for gauging architectural judgment around testability and coupling.

Dependency injection is like a restaurant kitchen that receives ingredients delivered by whichever supplier the manager has currently arranged, rather than each individual chef personally growing their own vegetables and raising their own livestock in the back alley — swapping to a different, better supplier (or a test 'fake' supplier for a recipe rehearsal) never requires retraining the chef's actual cooking technique, since the chef only ever depends on 'whatever ingredients get handed to me,' not any one specific supplier.

Key Concepts

1
Without DI, a function that needs to make an API call might directly import and call a specific fetch-based utility module internally, tightly coupling that function to one particular implementation — testing it in isolation then requires mocking a module-level import (awkward, and dependent on your test tooling's specific module-mocking capabilities), and swapping the implementation (say, for a different HTTP client, or a fake one in a different environment) means editing the function's internals directly. With DI, that same function instead accepts the HTTP-calling function as a parameter, and the *caller* decides which concrete implementation to actually pass in — production code passes the real fetch-based utility, while test code simply passes a plain mock function directly as an ordinary argument, with zero special module-mocking machinery required at all.
fetch
2
In JavaScript specifically, because functions are first-class values, 'dependency injection' at the function level is often indistinguishable from simply accepting a function as a parameter — much like the Strategy pattern covered elsewhere, DI in JS frequently doesn't require any dedicated DI-container framework or special syntax at all, just deliberate parameter design that avoids hardcoding specific concrete dependencies inside a function's own body.
3
More elaborate 'DI container' frameworks (like NestJS's built-in DI system, drawing heavily from Angular's and Java Spring's approach) exist for larger backend applications, automatically resolving and injecting a graph of interdependent services based on type annotations or decorators, which becomes genuinely valuable once an application has many services depending on each other in complex ways — but the underlying principle at any scale is the same: depend on explicitly passed-in references rather than internally constructed or directly imported concrete implementations, which is exactly what makes substituting mocks, fakes, or alternate implementations straightforward without touching the dependent code's internals.