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 means a module receives what it needs instead of reaching for it.

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
    // reaching out -- hard to test
    import { db } from './database.js';
    export function getUser(id) { return db.query(...); }
2
    // injected -- easy
    export function createUserService(db) {
      return { getUser: (id) => db.query(...) };
    }
3
Why the second is testable.
    const service = createUserService({ query: () => fakeUser });
4
No mocking framework, no module interception. Pass a different object.
5
The first version requires mocking the module itself — jest.mock('./database.js') — which is slower, fragile, and couples the test to the file layout.
jest.mock('./database.js')
6
It also makes dependencies visible. The signature says what this needs. Reaching for imports hides it, so you discover the coupling by reading the body.
7
In JavaScript, a function argument is usually enough. Other ecosystems need a container and decorators; here composition at startup does it:
8
    // wiring, in one place
    const db = createDatabase(config);
    const cache = createCache(config);
    const userService = createUserService(db, cache);
    const app = createApp(userService);
9
That file is the dependency graph, readable top to bottom.
10
Containers exist for larger applications. Angular's injector and NestJS's module system resolve dependencies from types, which scales better than manual wiring past a certain size — and costs some directness in exchange.
11
Defaults keep the common case convenient.
    export function createUserService(db = defaultDb) { }
12
Production callers pass nothing; tests pass a fake. That gets most of the benefit with none of the wiring ceremony.
13
What not to inject. Pure utilities — a date formatter, a validation helper — have no state and no I/O. Injecting them adds indirection for nothing.
14
The things worth injecting are those with side effects or external dependencies: databases, HTTP clients, clocks, file systems, loggers.
15
The clock is the one people forget.
    function isExpired(t, now = Date.now) { return t < now(); }
16
A test can then control time, instead of waiting or mocking a global — which is why Date.now being injectable turns a flaky test into a deterministic one.
Date.now
17
What the interviewer is probing.1. "What is dependency injection, simply?" Probing: the definition. Stalls: "A framework feature." Moves up: passing a dependency in rather than constructing it inside, so the caller chooses the implementation.
18
2. "Why does it matter for testing?" Probing: the payoff. Stalls: "It is cleaner." *Moves up:* a test supplies a stub without module mocking, so tests are faster and the dependency is explicit in the signature.
19
3. "Do you need a container?" Probing: proportionality. Stalls: "Yes, always." Moves up: rarely in JavaScript — passing arguments or a factory is usually enough; containers earn their place in large applications with deep graphs.
20
4. "How does module mocking compare?" Probing: the alternative. Stalls: "It is the same." Moves up: it works but couples tests to module paths and bundler behaviour, where injection is plain code.