Injecting interfaces instead of concrete classes in NestJS constructors (or any DI-based architecture) promotes decoupling, flexibility, and testability. However, because interfaces don't exist at runtime in TypeScript/JavaScript, this approach has practical limitations and requires additional setup (like useClass
, useExisting
, or useFactory
).
✅ Benefits of Injecting Interfaces Instead of Classes
1. Loose Coupling
- Problem: Injecting concrete classes tightly couples your service to a specific implementation.
- Benefit: Injecting an interface allows you to depend on a contract, not a specific implementation.
- Example:
constructor(private readonly userService: IUserService) {}
2. Improved Testability
- You can easily mock implementations in tests by providing a test-specific class or object.
- Example:
providers: [
{
provide: IUserService,
useClass: MockUserService,
},
]
3. Implementation Swapping
- Swap one implementation for another without changing the consuming code.
- Useful for different environments (e.g., mock vs. real, in-memory vs. database, local vs. cloud).
4. Domain-Driven Design (DDD) Alignment
- Encourages defining clear contracts (
interfaces
) between domain services, infrastructure, and application layers. - Supports clean architecture and separation of concerns.
5. Avoid Circular Dependencies
- When using interfaces and
useClass
oruseExisting
, you can often structure the code to avoid circular imports.
⚠️ Caveat in TypeScript and NestJS
- TypeScript interfaces do not exist at runtime, so you cannot inject an interface directly using the interface name alone.
- You must use tokens, often created with a
Symbol
orInjectionToken
.
🛠 How to Inject an Interface in NestJS
// user-service.interface.ts
export interface IUserService {
getUserById(id: string): Promise<User>;
}
// create a token
export const IUserServiceToken = Symbol('IUserService');
// in module
{
provide: IUserServiceToken,
useClass: UserService, // or a mock in test
}
// in constructor
constructor(@Inject(IUserServiceToken) private readonly userService: IUserService) {}
🧠 Summary
Benefit | Explanation |
---|---|
🔗 Loose Coupling | Decouples consumer from implementation |
🧪 Testability | Easier mocking and unit testing |
🔄 Flexibility | Swap implementations easily |
📐 Clean Architecture | Enforces contracts, aligns with DDD |
🔁 Avoid Circular Dependencies | Reduces tight runtime coupling |
Top comments (10)
I like to use the abstract classes typescript feature as interfaces so I can use it as a token for the provider as well and avoid the
@Inject()
decorator. For example:Good point, I didn’t think of it before — thanks!
But I want to clarify something :
For the Abstract Classes :
✖ Cons:
For the Interfaces :
✔ Pros:
Even if in the class constructor you specify a specific class, it does not mean that you are making a specific implementation of the interface. Whatever you specify in the class constructor, DI always tries to use it as a token.
Only when you pass a specific value, for example in the module metadata, do you define a specific implementation for a specific token. Using an interface instead of a class in the service constructor only makes sense when you are using a third-party library written without using TypeScript.
Interfaces add value when :
You can do all of the above without an interface.
how you do Swapping providers without changing service code using concrete classes ?
And how can you avoid circular dependency (take into consideration that using forwardRef to avoid circular dependency is not the best practice because it has its cons ) ?
Can you make an example to clarify more ?
Swapping
UserService1
byUserService2
:Oh, I see now, but this implementation is a little bit confusing , using abstract classes or interfaces is more organized and clearer I think .
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more