Example
Let’s walk through a practical scenario to see how the Dependency Inversion Principle can be applied. Suppose we’re building an app that examines repository data from GitHub. Our immediate goal is to expose an endpoint that reports how many pull requests are currently open in a given repository.
Here’s a straightforward way to implement that feature—but it doesn’t follow DIP:
import { Controller, Get, HttpModule, HttpService } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PullRequest } from 'app/domain';
@Controller()
export class AppController {
constructor(private http: HttpService) {}
@Get('repository/:id/pending-prs')
getNumberOfPendingPrs(id: string): Observable<number> {
return this.http
.get<PullRequest[]>(`https://api.github.com/repos/${id}/pulls`)
.pipe(map(res => res.data.length));
}
}
Simply relocating that HTTP request into a dedicated service doesn’t truly conceal the data-fetching details from the controller either:
import { Controller, Get, HttpService } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { GithubService } from 'app/infrastructure';
@Controller()
export class AppController {
constructor(private githubService: GithubService) {}
@Get('repository/:id/pending-prs')
getNumberOfPendingPrs(id: string): Observable<number> {
return this.githubService.getPullRequests(id).pipe(map(prs => prs.length));
}
}
import { HttpService, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PullRequest } from 'app/domain';
@Injectable()
export class GithubService {
constructor(private http: HttpService) {}
getPullRequests(id: string): Observable<PullRequest[]> {
return this.http
.get<PullRequest[]>(`https://api.github.com/repos/${id}/pulls`)
.pipe(map(res => res.data));
}
}
Applying the Dependency Inversion Principle
To do it correctly, we need to set things up like this:
import { Controller, Get } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { RepositoryService } from 'app/interfaces';
@Controller()
export class AppController {
constructor(private repositoryService: RepositoryService) {}
@Get('repository/:id/pending-prs')
getNumberOfPendingPrs(id: string): Observable<number> {
return this.repositoryService
.getPullRequests(id)
.pipe(map(prs => prs.length));
}
}
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { GithubInfrastructureModule } from 'app/infrastructure-github';
@Module({ imports: [GithubInfrastructureModule], controllers: [AppController] })
export class AppModule {}
import { HttpModule, Module } from '@nestjs/common';
import { RepositoryService } from 'app/interfaces';
import { GithubRepositoryService } from './github-repository.service';
@Module({
imports: [HttpModule],
providers: [
{ provide: RepositoryService, useClass: GithubRepositoryService }
],
exports: [RepositoryService]
})
export class GithubInfrastructureModule {}
import { HttpService, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PullRequest } from 'app/domain';
import { RepositoryService } from 'app/interfaces';
@Injectable()
export class GithubRepositoryService implements RepositoryService {
constructor(private http: HttpService) {}
getPullRequests(id: string): Observable<PullRequest[]> {
return this.http
.get<PullRequest[]>(`https://api.github.com/repos/${id}/pulls`)
.pipe(map(res => res.data));
}
}
import { Observable } from 'rxjs';
import { PullRequest } from 'app/domain';
export abstract class RepositoryService {
abstract getPullRequests(id: string): Observable<PullRequest[]>;
}
- We introduce an abstraction for retrieving pull request data, defined by the
RepositoryServiceabstract class. - Within the
AppController, we request an injection of something that sits behind theRepositoryServicetoken. - The
GithubInfrastructureModuledeclares thatGithubRepositoryServiceshould be supplied in that context.
So why can’t a plain interface serve as our abstraction here?
The reason lies in what happens to TypeScript interfaces when the code is compiled to JavaScript. All knowledge of the RepositoryService interface is erased, as is the detail about what should be injected into the AppController. In practice, this means an interface cannot be supplied as a provider value within a module.
Classes behave differently, though. Even an abstract class ends up as a regular class after compilation, which means it can act as an injection token. In TypeScript, a class can also be implemented by another class, mirroring how interfaces work—but with the added benefit of surviving transpilation.
Why This Matters
Now imagine the requirements shift. We need to support repositories from Bitbucket in a separate application instance. Without the abstraction in place what was done early, we’d find ourselves inserting a bunch of ifs across services and controllers to decide which HTTP call to make for each data source.
Because the data source layer is neatly tucked away, we can add a dedicated module for Bitbucket services and wire up our feature module in the right way:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { GithubInfrastructureModule } from 'app/infrastructure-github';
import { BitbucketInfrastructureModule } from 'app/infrastructure-bitbucket';
@Module({
imports: [
...(process.env.provider === 'GITHUB' ? [GithubInfrastructureModule] : []),
...(process.env.provider === 'BITBUCKET' ? [BitbucketInfrastructureModule] : [])
],
controllers: [AppController]
})
export class AppModule {}
Depending on the environment, either GithubRepositoryService or BitbucketRepositoryService gets injected into the AppController, without requiring any modifications to the outer layers.
In an Angular context, this approach proves valuable when:
- You’re developing separate web and mobile apps that load data through different mechanisms.
- You’re implementing SSR. On the server side, you want to persist incoming data into the
TransferState. On the browser side, you prefer to read it from there instead.
In both situations, adhering to the Dependency Inversion Principle helps minimize the changes and complexity in high-level modules that interact with the data access layer. The key nuance is that we shouldn’t rely on the environment to pick which module to import—we just define the dependency clearly.
Summary
Embracing SOLID principles comes with a host of advantages. They contribute to a codebase that is easier to reuse, maintain, scale, and test. Both Nest and Angular make it straightforward to apply these principles in a clean and elegant way.
If you’re curious about how to use GithubRepositoryService and BitbukcetRepositoryService together within one application instance, check out this repository.
In the end, the claim in Nest’s documentation holds up as a reality.
