DEV Community

Cover image for Why You Should Inject Interfaces, Not Classes, in NestJS Applications ?
bilel salem
bilel salem

Posted on

Why You Should Inject Interfaces, Not Classes, in NestJS Applications ?

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) {}
Enter fullscreen mode Exit fullscreen mode

2. Improved Testability

  • You can easily mock implementations in tests by providing a test-specific class or object.
  • Example:
  providers: [
    {
      provide: IUserService,
      useClass: MockUserService,
    },
  ]
Enter fullscreen mode Exit fullscreen mode

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 or useExisting, 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 or InjectionToken.

🛠 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) {}
Enter fullscreen mode Exit fullscreen mode

🧠 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)

Collapse
 
micalevisk profile image
Micael Levi L. C.

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:

export abstract class IUserService {
  abstract getUserById(id: string): Promise<User>;
}

export class UserService implements IUserService {
  getUserById(id: string): Promise<User> {
    // ...
  }
}

@Module({
  providers: [
    {
      provide: IUserService,
      useClass: UserService,
    }
  ]
})
export class AppModule {
  constructor(private readonly userService: IUserService) {}
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bilelsalemdev profile image
bilel salem • Edited

Good point, I didn’t think of it before — thanks!
But I want to clarify something :
For the Abstract Classes :
✖ Cons:

  • Not a true interface: Abstract classes can have implementation details or state, which may be unwanted if you're strictly defining a contract.
  • Tight coupling to class inheritance: Your implementation is forced to extend the abstract class.

For the Interfaces :
✔ Pros:

  • Pure abstraction: Interfaces are cleaner contracts — no logic, just structure.
  • More flexible: You can implement multiple interfaces, which isn't possible with class inheritance.
  • Better alignment with Domain-Driven Design (DDD): Interfaces are ideal for defining service boundaries.
Collapse
 
kostyatretyak profile image
Костя Третяк

Problem: Injecting concrete classes tightly couples your service to a specific implementation.

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.

Collapse
 
bilelsalemdev profile image
bilel salem

Interfaces add value when :

  • Creating mock implementations for testing
  • Swapping providers without changing service code
  • Library interoperability (as you noted)
Collapse
 
kostyatretyak profile image
Костя Третяк

You can do all of the above without an interface.

Thread Thread
 
bilelsalemdev profile image
bilel salem • Edited

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 ) ?

Thread Thread
 
kostyatretyak profile image
Костя Третяк
  1. In the service constructor you specify the token, and in the module metadata you pass the class you want. Are you talking about a different swap?
  2. Cyclic dependencies should be avoided, and if they do occur, forwardRef will solve the problem. This should occur quite rarely, so there shouldn't be any particular problems here.
Thread Thread
 
bilelsalemdev profile image
bilel salem

Can you make an example to clarify more ?

Thread Thread
 
kostyatretyak profile image
Костя Третяк • Edited

Swapping UserService1 by UserService2:


@Injectable()
class UserService1 {
  getUserById(id: string): Promise<User> {
    // ... Some implementation
  }
}

@Injectable()
class UserService2 {
  getUserById(id: string): Promise<User> {
    // ... Other implementation
  }
}

@Module({
  providers: [
    {
      provide: UserService1,
      useClass: UserService2,
    }
  ]
})
class AppModule {
  constructor(private readonly userService: UserService1) {}
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
bilelsalemdev profile image
bilel salem

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