Objectives
By the end of this lesson, you should be able to:
- Spot a dependency in a frontend service.
- Explain why a service that reaches for
fetchis tightly coupled to the network. - Move the transport choice from the service to its caller with constructor injection.
- Replace a real transport with a small fake when you test the service.
Prerequisites
You need basic TypeScript knowledge. You should be comfortable with classes, functions, promises, and the browser fetch API.
1. Spot the dependency
Frontend code uses many things that a module did not create and cannot control: fetch, localStorage, the system clock, and API clients. Consider this small price loader:
class PriceService {
async loadPrice(productId: string): Promise<number> {
const response = await fetch(`/api/price/${productId}`);
const data = await response.json();
return data.priceUsd as number;
}
}
Ask one question. What does this class need that it does not own?
The answer is the network. fetch is a dependency: something the module needs to do its job but should not decide for itself.
The class works, but two choices are now welded together:
- Every use of
PriceServiceperforms real HTTP. - A test of
loadPriceneeds the network or must monkey-patchglobalThis.fetch.
Confusing use with ownership
A class can use a dependency without owning the decision about which implementation to use. If changing the transport means editing the service, the service is coupled to a concrete detail.
Coupling is how far a change travels. Here, changing how prices are fetched means editing the class itself. The service and transport are tightly coupled because they share concrete details, not only a job description. See the coupling glossary entry.
2. Move the dependency into the constructor
Dependency injection needs no framework or container. The class declares what it needs, and the caller supplies it:
class PriceService {
// Needs a function: id -> price. That is all.
constructor(private readonly fetchPrice: (id: string) => Promise<number>) {}
async loadPrice(productId: string): Promise<number> {
return this.fetchPrice(productId);
}
}
The function parameter is constructor injection. The choice of transport moved out of the class and to the code that builds it.
The caller can now wire the real transport at the application edge:
const service = new PriceService(async (id) => {
const response = await fetch(`/api/price/${id}`);
return (await response.json()).priceUsd as number;
});
The service changed in one place. Before, it fetched its dependency. After, it received that dependency from its caller.
Give, do not take. That is the essential idea of dependency injection. Fowler describes the pattern and its history.
No interface was introduced, and none was needed. Interfaces and abstractions are a separate design move covered by the next planned lesson.
3. See the difference
The service below is unchanged between the two examples. Only the constructor argument changes. One transport uses the network; the other is an offline fake.
Explanatory network-dependent example
This transport can fail when the page has no /api/price/:id endpoint. It is shown to explain the coupling; it is not an offline demo and does not run in this lesson:
const realTransport = (id: string): Promise<number> =>
fetch(`/api/price/${id}`)
.then((response) => {
if (!response.ok) {
throw new Error(`transport failed (HTTP ${response.status})`);
}
return response.json();
})
.then((data) => data.priceUsd as number);
const networkService = new PriceService(realTransport);
Offline fake
This fake returns deterministic values and never calls the network:
const fakeTransport = async (id: string): Promise<number> =>
({ "p-042": 1290, "p-007": 499 })[id] ?? 0;
const offlineService = new PriceService(fakeTransport);
const price = await offlineService.loadPrice("p-042");
// 1290
Run the service with an injected transport
The class stays the same. Choose a transport to see how the wiring changes its behaviour.
Explanatory network-dependent example. The real transport is shown above but never runs here. Simulate its failure to see the same coupling without a network request.
$ waiting. Choose a transport.
The class source stayed the same. The behaviour changed at the wiring. That is testability as a result of the design, not of a mocking tool.
4. Retrieve the idea
Try to answer these questions before opening the model answers:
- In the terms used in this lesson, what counts as a dependency of a module?
- What exactly does constructor injection change in the
aftercode? - What lets one class produce two different behaviours?
- Which symptom most directly shows that the
beforeclass is tightly coupled?
Show model answers
- A dependency is something the module needs but does not own or control, such as
fetch, storage, or the clock. - The dependency arrives as a constructor parameter, so the class signature tells the truth about what it needs.
- The caller swaps the constructor argument while the class source remains unchanged.
- Tests need the real network, so the service cannot be exercised without its concrete transport.
"Dependency injection is..." Finish the sentence
It is a wiring technique where objects receive dependencies from outside, usually through the constructor, instead of creating or fetching them. A framework is optional.
Why does injection improve testability?
The class no longer owns the transport. A test can construct it with a fake, without network access or monkey-patching, so the test exercises the service logic.
Exercises
- Find a browser API used directly inside one of your services. Write down what the service needs and what choice it currently owns.
- Replace that direct call with a function constructor parameter. Keep the parameter as small as the service actually needs.
- Create a deterministic fake and use it to exercise the service without a network request.
- Explain which code is now the composition point: the service or the caller that constructs it?
Further reading
- Dependency Injection vs Dependency Inversion vs Inversion of Control, SSENSE Tech. This article walks through a TypeScript refactor.
- InversionOfControl, Martin Fowler. It explains the question of who calls whom.
- DIP in the Wild, Brett L. Schuchert. It separates wiring, direction, and shape.
- Review the course glossary and the further reading before the next lesson.
Citations
- Fowler, Inversion of Control Containers and the Dependency Injection pattern. This article explains DI and constructor injection.
- Schuchert, DIP in the Wild. This article explains coupling and dependency wiring.
- SSENSE Tech, DI vs Dependency Inversion vs IoC. This article compares DI, dependency inversion, and IoC in TypeScript.