NestJS Logo

Custom providers

Earlier chapters touched on various aspects of dependency injection (DI) and how Nest uses it. One example is constructor-based dependency injection, which injects instances (often service providers) into classes. Dependency injection is built into the Nest core at a fundamental level. So far, we've explored only one main pattern. As your application grows more complex, you may need the full feature set of the DI system, so this chapter explores it in more detail.

DI fundamentals#

Dependency injection is an inversion of control (IoC) technique in which you delegate the instantiation of dependencies to the IoC container (in our case, the NestJS runtime system) instead of instantiating them imperatively in your own code. Let's examine what happens in this example from the Providers chapter.

First, we define a provider. The @Injectable() decorator marks the CatsService class as a provider.

cats.service.ts
JS TS

import { Injectable } from '@nestjs/common';
import type { Cat } from './interfaces/cat.interface.js';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  findAll(): Cat[] {
    return this.cats;
  }
}

import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  constructor() {
    this.cats = [];
  }

  findAll() {
    return this.cats;
  }
}

Then we request that Nest inject the provider into our controller class:

cats.controller.ts
JS TS

import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service.js';
import type { Cat } from './interfaces/cat.interface.js';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

import { Controller, Get, Dependencies } from '@nestjs/common';
import { CatsService } from './cats.service.js';

@Controller('cats')
@Dependencies(CatsService)
export class CatsController {
  constructor(catsService) {
    this.catsService = catsService;
  }

  @Get()
  async findAll() {
    return this.catsService.findAll();
  }
}

Finally, we register the provider with the Nest IoC container:

app.module.ts
JS TS

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller.js';
import { CatsService } from './cats/cats.service.js';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

Three key steps make this work:

  1. In cats.service.ts, the @Injectable() decorator declares CatsService as a class that the Nest IoC container can manage.
  2. In cats.controller.ts, CatsController declares a dependency on the CatsService token with constructor injection:

  constructor(private catsService: CatsService)
  1. In app.module.ts, we associate the token CatsService with the class CatsService from the cats.service.ts file. The Standard providers section below shows exactly how this association (also called registration) occurs.

When the Nest IoC container instantiates a CatsController, it first looks for any dependencies*. When it finds the CatsService dependency, it looks up the CatsService token, which returns the CatsService class, per the registration step (#3 above). Assuming SINGLETON scope (the default), Nest then either creates an instance of CatsService, caches it, and returns it, or, if an instance is already cached, returns the existing one.

*This explanation is simplified to illustrate the point. In reality, analyzing the code for dependencies is a sophisticated process that happens during application bootstrapping. One key property is that dependency analysis (or "creating the dependency graph") is transitive: in the example above, if CatsService itself had dependencies, those would be resolved too. The dependency graph ensures that dependencies are resolved in the correct order, essentially "bottom up". This mechanism relieves you from having to manage complex dependency graphs yourself.

Standard providers#

Let's take a closer look at the @Module() decorator. In app.module, we declare:


@Module({
  controllers: [CatsController],
  providers: [CatsService],
})

The providers property takes an array of providers. So far, we've supplied those providers as a list of class names. In fact, the syntax providers: [CatsService] is shorthand for the more complete syntax:


providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
];

This explicit construction shows the registration process: it associates the token CatsService with the class CatsService. The shorthand notation is a convenience for the most common use case, where the token is used to request an instance of the class with the same name.

Custom providers#

Sometimes your requirements go beyond what standard providers offer. For example:

  • You want to create a custom instance instead of having Nest instantiate (or return a cached instance of) a class.
  • You want to reuse an existing class in a second dependency.
  • You want to override a class with a mock version for testing.

Nest lets you define custom providers to handle these cases, and offers several ways to define them. The following sections walk through each one.

Hint If you run into problems with dependency resolution, set the NEST_DEBUG environment variable to get extra dependency resolution logs during startup.

Value providers: useValue#

The useValue syntax is useful for injecting a constant value, putting an external library into the Nest container, or replacing a real implementation with a mock object. For example, suppose you want Nest to use a mock CatsService for testing purposes:


import { CatsService } from './cats.service.js';

const mockCatsService = {
  /* mock implementation
  ...
  */
};

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

In this example, the CatsService token resolves to the mockCatsService mock object. useValue requires a value, in this case a literal object that has the same interface as the CatsService class it replaces. Because of TypeScript's structural typing, you can use any object with a compatible interface, including a literal object or a class instance created with new.

Non-class-based provider tokens#

So far, we've used class names as our provider tokens (the value of the provide property in a provider listed in the providers array). This matches the standard pattern used with constructor-based injection, where the token is also a class name. (See DI fundamentals for a refresher on tokens.) Sometimes you need the flexibility to use strings or symbols as DI tokens. For example:


import { connection } from './connection.js';

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

In this example, we associate a string-valued token ('CONNECTION') with a pre-existing connection object imported from an external file.

Notice Besides strings, you can also use JavaScript symbols or TypeScript enums as token values.

The standard constructor-based injection pattern requires the dependency to be declared with a class name. The 'CONNECTION' custom provider, however, uses a string-valued token. To inject such a provider, use the @Inject() decorator, which takes a single argument: the token.

JS TS

@Injectable()
export class CatsRepository {
  constructor(@Inject('CONNECTION') connection: Connection) {}
}

@Injectable()
@Dependencies('CONNECTION')
export class CatsRepository {
  constructor(connection) {}
}
Hint The @Inject() decorator is imported from the @nestjs/common package.

The examples above use the string 'CONNECTION' directly for illustration. For clean code organization, define tokens in a separate file, such as constants.ts, and import them where needed, as you would with symbols or enums.

Interfaces and abstract classes#

TypeScript types and interfaces are erased during compilation, so Nest can't reference them at runtime. An interface can describe the shape of a dependency, but it can't serve as a DI token by itself.

Because Nest resolves providers by runtime tokens, use a string or Symbol token when registering a provider for an interface:


export interface LoggerService {
  log(message: string): void;
}

export const LOGGER_SERVICE = Symbol('LOGGER_SERVICE');

@Injectable()
export class PinoLoggerService implements LoggerService {
  log(message: string) {
    // implementation details
  }
}

@Module({
  providers: [
    {
      provide: LOGGER_SERVICE,
      useClass: PinoLoggerService,
    },
  ],
})
export class AppModule {}

To inject this provider, pass that token to the @Inject() decorator:


@Injectable()
export class CatsService {
  constructor(
    @Inject(LOGGER_SERVICE)
    private readonly logger: LoggerService,
  ) {}
}

Abstract classes, unlike interfaces, exist at runtime. You can use an abstract class as both the TypeScript contract and the DI token:


export abstract class LoggerService {
  abstract log(message: string): void;
}

@Injectable()
export class PinoLoggerService implements LoggerService {
  log(message: string) {
    // implementation details
  }
}

@Module({
  providers: [
    {
      provide: LoggerService,
      useClass: PinoLoggerService,
    },
  ],
})
export class AppModule {}

With an abstract class token, constructor-based injection uses the abstract class type directly and doesn't require @Inject():


@Injectable()
export class CatsService {
  constructor(private readonly logger: LoggerService) {}
}

Use string or Symbol tokens when the runtime DI token should be decoupled from a class. Symbol tokens are especially useful for libraries and larger applications: each symbol has a unique runtime identity, which avoids the accidental collisions that can occur when unrelated providers use the same string token. When using a symbol token, export it from a shared file and reuse the same symbol instance wherever the provider is registered and injected. Use an abstract class when one artifact should act as both the contract and the runtime token, and you prefer simpler constructor injection. A plain interface remains a good choice when the type is used only for compile-time checking and no DI token is needed.

Class providers: useClass#

The useClass syntax lets you dynamically determine the class that a token resolves to. For example, suppose we have an abstract (or default) ConfigService class, and we want Nest to provide a different implementation of the configuration service depending on the current environment. The following code implements this strategy:


const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
};

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

Two details in this sample are worth noting. First, we define configServiceProvider as a literal object, then pass it in the module decorator's providers property. This is only code organization; it is functionally equivalent to the examples used so far in this chapter.

Second, we use the ConfigService class name as the token. For any class that depends on ConfigService, Nest injects an instance of the provided class (DevelopmentConfigService or ProductionConfigService), overriding any default implementation that may have been declared elsewhere (e.g., a ConfigService declared with an @Injectable() decorator).

Factory providers: useFactory#

The useFactory syntax lets you create providers dynamically. The provider's value is whatever the factory function returns. The factory function can be as simple or as complex as needed. A simple factory may not depend on any other providers. A more complex factory can inject the providers it needs to compute its result. For the latter case, the factory provider syntax has a pair of related mechanisms:

  1. The factory function can accept (optional) arguments.
  2. The (optional) inject property accepts an array of providers that Nest resolves and passes as arguments to the factory function during instantiation. These providers can also be marked as optional. The two lists are correlated: Nest passes the instances from the inject list to the factory function as arguments, in the same order. The example below demonstrates this.
JS TS

const connectionProvider = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: MyOptionsProvider, optionalProvider?: string) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [MyOptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
  //       \______________/             \__________________/
  //        This provider                The provider with this token
  //        is mandatory.                can resolve to `undefined`.
};

@Module({
  providers: [
    connectionProvider,
    MyOptionsProvider, // class-based provider
    // { provide: 'SomeOptionalProvider', useValue: 'anything' },
  ],
})
export class AppModule {}

const connectionProvider = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider, optionalProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [MyOptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
  //       \______________/            \__________________/
  //        This provider               The provider with this token
  //        is mandatory.               can resolve to `undefined`.
};

@Module({
  providers: [
    connectionProvider,
    MyOptionsProvider, // class-based provider
    // { provide: 'SomeOptionalProvider', useValue: 'anything' },
  ],
})
export class AppModule {}

Alias providers: useExisting#

The useExisting syntax lets you create aliases for existing providers, which gives you two ways to access the same provider. In the example below, the (string-based) token 'AliasedLoggerService' is an alias for the (class-based) token LoggerService. Assume we have two different dependencies, one on 'AliasedLoggerService' and one on LoggerService. If both dependencies are specified with SINGLETON scope, they both resolve to the same instance.


@Injectable()
class LoggerService {
  /* implementation details */
}

const loggerAliasProvider = {
  provide: 'AliasedLoggerService',
  useExisting: LoggerService,
};

@Module({
  providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

Non-service based providers#

Providers often supply services, but they aren't limited to that. A provider can supply any value. For example, a provider can supply a configuration object based on the current environment:


const configFactory = {
  provide: 'CONFIG',
  useFactory: () => {
    return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
  },
};

@Module({
  providers: [configFactory],
})
export class AppModule {}

Export custom provider#

Like any provider, a custom provider is scoped to its declaring module. To make it visible to other modules, you must export it, using either its token or the full provider object.

The following example exports the provider by its token:

JS TS

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: ['CONNECTION'],
})
export class AppModule {}

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: ['CONNECTION'],
})
export class AppModule {}

Alternatively, export the full provider object:

JS TS

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: [connectionFactory],
})
export class AppModule {}

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: [connectionFactory],
})
export class AppModule {}
Edit on GitHub

Support us

Nest is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you'd like to join them, please read more here.

Principal Sponsors

SerpApi LogoTrilon LogoMojam Logo

Sponsors / Partners

Become a sponsor