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

bilel salem @bilelsalemdev

About: full stack developer

Location:
Tunisia
Joined:
May 20, 2024

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

Publish Date: Jun 14
3 11

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

Comments 11 total

  • Micael Levi L. C.
    Micael Levi L. C.Jun 14, 2025

    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
    • bilel salem
      bilel salemJun 15, 2025

      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.
  • АнонимJun 14, 2025

    [hidden by post author]

  • Костя Третяк
    Костя ТретякJun 15, 2025

    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.

    • bilel salem
      bilel salemJun 15, 2025

      Interfaces add value when :

      • Creating mock implementations for testing
      • Swapping providers without changing service code
      • Library interoperability (as you noted)
      • Костя Третяк
        Костя ТретякJun 15, 2025

        You can do all of the above without an interface.

        • bilel salem
          bilel salemJun 15, 2025

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

          • Костя Третяк
            Костя ТретякJun 15, 2025
            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.
            • bilel salem
              bilel salemJun 15, 2025

              Can you make an example to clarify more ?

              • Костя Третяк
                Костя ТретякJun 16, 2025

                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
                • bilel salem
                  bilel salemJun 16, 2025

                  Oh, I see now, but this implementation is a little bit confusing , using abstract classes or interfaces is more organized and clearer I think .

Add comment