Nestjs Provider

Nestjs Provider

Post Date : 2022-07-28T14:41:08+07:00

Modified Date : 2022-07-28T14:41:08+07:00

Category: nestjs frameworks

Tags: nestjs

Note: All of demo source code you can find our in github nestjs boilerplate

Many of the basic Nest classes may be treated as a provider:

  • Services
  • Repositories
  • Factories
  • Helpers

The main idea of a provider is that it can be injected as a dependency

This means NestJS will be act as a container and creating relationship between objects and wire them up at runtime

When creating a provider, we should consider these aspects

  • Dependency Inject
  • Scopes
  • Custom provider
  • Optional providers

Using service provider in controller

Creating service

@Injectable() : injectable decorator

export interface Example {
  id: string;
  title: string;
  keywords?: Array<string>;
  content: string;
  tags: Array<string>;
  createdAt: Date;
  updatedAt: Date;
}

import { Example } from "./../interfaces/example.interface";
import { Injectable } from "@nestjs/common";
import { v4 as uuidv4 } from "uuid";

@Injectable()
export class ExampleService {
  static initialTimes = 0;
  constructor() {
    ExampleService.initialTimes++;
    console.log(
      `initExampleService times: ${ExampleService.initialTimes} `,
      new Date().toISOString()
    );
  }
  create(payload: Partial<Example>): Example {
    const now = new Date();
    return {
      id: uuidv4(),
      title: "",
      keywords: [],
      content: "",
      tags: [],
      createdAt: now,
      updatedAt: now,
      ...payload,
    };
  }
}

Register as a module’s provider

import { Module } from "@nestjs/common";
import { ExamplesController } from "./controllers/examples.controller";
import { ExampleService } from "./services/example.service";

@Module({
  controllers: [ExamplesController],
  providers: [ExampleService], // add service to module's providers
})
export class ExampleModule {}

Use

import { Example } from "./../interfaces/example.interface";
import { ExampleService } from "./../services/example.service";
import { Controller, Post } from "@nestjs/common";
import { Body } from "@nestjs/common";

@Controller("examples")
export class ExamplesController {
  constructor(private exampleService: ExampleService) {}

  @Post("")
  create(@Body() payload): Example {
    const example: Example = this.exampleService.create(payload);
    return example;
  }
}

Understand scopes

Providers normally have a lifetime (“scope”) synchronized with the application lifecycle. When the application is bootstrapped, every dependency must be resolved, and therefore every provider has to be instantiated

There are 3 type of scopes

  • DEFAULT : A single instance of the provider is shared across the entire application
  • REQUEST : A new instance of the provider is created exclusively for each incoming request
  • TRANSIENT: Transient providers are not shared across consumers. Each consumer that injects a transient provider will receive a new, dedicated instance

Let’s do some test

DEFAULT scope

[Nest] 13408  - 07/28/2022, 3:54:44 PM     LOG [NestFactory] Starting Nest application...
initExampleService times: 1  2022-07-28T08:54:44.622

REQUEST scope

[Nest] 16160  - 07/28/2022, 3:57:00 PM     LOG [RouterExplorer] Mapped {/examples/:id, GET} route +2ms
[Nest] 16160  - 07/28/2022, 3:57:00 PM     LOG [RouterExplorer] Mapped {/examples, POST} route +1ms
[Nest] 16160  - 07/28/2022, 3:57:00 PM     LOG [RouterExplorer] Mapped {/examples/:id, PATCH} route +2ms
[Nest] 16160  - 07/28/2022, 3:57:00 PM     LOG [RouterExplorer] Mapped {/examples/:id, DELETE} route +2ms
[Nest] 16160  - 07/28/2022, 3:57:00 PM     LOG [NestApplication] Nest application successfully started +5ms
initExampleService times: 1  2022-07-28T08:57:24.453Z
initExampleService times: 2  2022-07-28T08:57:28.310Z
initExampleService times: 3  2022-07-28T08:57:32.456Z
initExampleService times: 4  2022-07-28T08:57:36.250Z
initExampleService times: 5  2022-07-28T08:57:39.503Z

TRANSIENT scope

Let’s create a new controller and use the ExampleService

import { ExampleService } from "./../services/example.service";
import { Controller } from "@nestjs/common";

@Controller("example-groups")
export class ExampleGroupsController {
  constructor(private exampleService: ExampleService) {}
  index() {
    return [];
  }
}

And we got

[Nest] 19024  - 07/28/2022, 4:02:46 PM     LOG [NestFactory] Starting Nest application...
[Nest] 19024  - 07/28/2022, 4:02:46 PM     LOG [InstanceLoader] AppModule dependencies initialized +63ms
[Nest] 19024  - 07/28/2022, 4:02:46 PM     LOG [InstanceLoader] GoldModule dependencies initialized +0ms
initExampleService times: 1  2022-07-28T09:02:46.846Z
initExampleService times: 2  2022-07-28T09:02:46.847Z

Custom provider

There are several ways to define a provider: you can use plain values, classes, and either asynchronous or synchronous factories

Standard Provider

import { Module } from "@nestjs/common";
import { ExamplesController } from "./controllers/examples.controller";
import { ExampleService } from "./services/example.service";

@Module({
  controllers: [ExamplesController],
  providers: [ExampleService], // add service to module's providers
})
export class ExampleModule {}

// is the short-hand of

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

Why do we need a customer provider?

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

How do we export custom providers for usage?

Like any provider, a custom provider is scoped to its declaring module. To make it visible to other modules, it must be exported. To export a custom provider, we can either use its token or the full provider object

Token Example

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 {}

Full Object Example

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 {}

How to To get existing instances, or instantiate providers dynamically?

Module Reference

@Injectable()
export class CatsService implements OnModuleInit {
  private service: Service;
  constructor(private moduleRef: ModuleRef) {}

  onModuleInit() {
    this.service = this.moduleRef.get(Service);
  }
}

Value providers

Using the same example. In this case uuid + now is generated at run time and different for each test. So the only way to test it is creating a mock of ExampleService.

    providers: [
    {
        provide: ExampleService,
        useValue: mockExampleService,
    },
    ],
import { ExampleService } from "./../services/example.service";
import { Test, TestingModule } from "@nestjs/testing";
import { ExamplesController } from "./examples.controller";
import { Example } from "@modules/example/interfaces/example.interface";
import { v4 as uuidv4 } from "uuid";

const mockExampleService = {
  /* mock implementation
  ...
  */
  create(payload: Partial<Example>): Example {
    const now = new Date("2022-07-28T08:59:02.246Z");
    return {
      id: "1170b8c2-630a-4ad6-842d-ac7255213cf6",
      title: "example 1",
      keywords: ["example"],
      content: "example",
      tags: ["nestjs"],
      createdAt: now,
      updatedAt: now,
      ...payload,
    };
  },
};

describe("ExamplesController", () => {
  let controller: ExamplesController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [ExamplesController],
      providers: [
        {
          provide: ExampleService,
          useValue: mockExampleService,
        },
      ],
    }).compile();

    controller = module.get<ExamplesController>(ExamplesController);
  });

  it("should be defined", () => {
    expect(controller).toBeDefined();
  });

  describe("Create new example", () => {
    it("should create a new example successfully if data is valid", () => {
      const now = new Date("2022-07-28T08:59:02.246Z");
      const responseValue = controller.create({
        title: "example 1",
        keywords: ["example"],
        content: "example",
        tags: ["nestjs"],
      });
      const expectedValue = {
        id: "1170b8c2-630a-4ad6-842d-ac7255213cf6",
        title: "example 1",
        keywords: ["example"],
        content: "example",
        tags: ["nestjs"],
        createdAt: now,
        updatedAt: now,
      };
      expect(responseValue).toMatchObject(expectedValue);
    });
  });
});

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 is matched by the standard pattern used with constructor based injection, where the token is also a class name

Sometimes, we may want the flexibility to use strings or symbols as the DI token

import dataProviders from "./providers";
import { Scope } from "@nestjs/common";
import { DynamicModule } from "@nestjs/common";

export const MONGODB_URL = Environment.getConfigValues().MONGODB_URL;
export const mongooseOptions: MongooseOptions = { autoCreate: true };

export const databaseProviders = [
  {
    scope: Scope.DEFAULT,
    provide: "DatabaseConnection",
    useFactory: async () =>
      await mongoose.connect(MONGODB_URL, mongooseOptions),
  },
  ...dataProviders,
];

console.log("MONGODB_URL", MONGODB_URL);

@Module({
  imports: [],
  providers: databaseProviders,
  exports: databaseProviders,
})
export class DatabaseModule {}

NestJS Mongoose

export function getConnectionToken(name?: string) {
  return name && name !== DEFAULT_DB_CONNECTION
    ? `${name}Connection`
    : DEFAULT_DB_CONNECTION;
}

const mongooseConnectionFactory =
  connectionFactory || ((connection) => connection);
const mongooseConnectionName = getConnectionToken(connectionName);

const mongooseConnectionNameProvider = {
  provide: MONGOOSE_CONNECTION_NAME,
  useValue: mongooseConnectionName,
};
const connectionProvider = {
  provide: mongooseConnectionName,
  useFactory: async (): Promise<any> =>
    await lastValueFrom(
      defer(async () =>
        mongooseConnectionFactory(
          await mongoose.createConnection(uri, mongooseOptions).asPromise(),
          mongooseConnectionName
        )
      ).pipe(handleRetry(retryAttempts, retryDelay))
    ),
};
return {
  module: MongooseCoreModule,
  providers: [connectionProvider, mongooseConnectionNameProvider],
  exports: [connectionProvider],
};

Custom injection key for connection

import { Module } from "@nestjs/common";
import { MongooseModule, MongooseModuleOptions } from "@nestjs/mongoose";
import mongoose, { MongooseOptions } from "mongoose";
import { Environment } from "@config/environment";

import dataProviders from "./providers";
import { Scope } from "@nestjs/common";
import { DynamicModule } from "@nestjs/common";

export const MONGODB_URL = Environment.getConfigValues().MONGODB_URL;
export const mongooseOptions: MongooseModuleOptions = {
  autoCreate: true,
  connectionName: "Default",
};

export const databaseProviders = [...dataProviders];

@Module({
  imports: [MongooseModule.forRoot(MONGODB_URL, mongooseOptions)],
  providers: databaseProviders,
  exports: databaseProviders,
})
export class DatabaseModule {}

// usage
@Inject('DefaultConnection')
private connection: Connection,

Class providers: useClass

The useClass syntax allows you to dynamically determine a class that a token should resolve to

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

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

Factory providers: useFactory

The useFactory syntax allows for creating providers dynamically. The actual provider will be supplied by the value returned from a factory function. The factory function can be as simple or complex as needed

Sometimes, you may need to create a provider that depends on others, so using Factory is a best approach here.

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

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

Alias providers : useExisting

The useExisting syntax allows you to create aliases for existing providers. This creates 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. If both dependencies are specified with SINGLETON scope, they’ll 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

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

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

Custom provider References

Optional providers

Occasionally, you might have dependencies which do not necessarily have to be resolved. For instance, your class may depend on a configuration object, but if none is passed, the default values should be used. In such a case, the dependency becomes optional, because lack of the configuration provider wouldn’t lead to errors.

import { Injectable, Optional, Inject } from "@nestjs/common";

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject("HTTP_OPTIONS") private httpClient: T) {}
}

Property-based injection

The technique we’ve used so far is called constructor-based injection, as providers are injected via the constructor method. In some very specific cases, property-based injection might be useful. For instance, if your top-level class depends on either one or multiple providers, passing them all the way up by calling super() in sub-classes from the constructor can be very tedious. In order to avoid this, you can use the @Inject() decorator at the property level.

import { Injectable, Inject } from "@nestjs/common";

@Injectable()
export class HttpService<T> {
  @Inject("HTTP_OPTIONS")
  private readonly httpClient: T;
}

Final

It is up to you for choosing the right recipes for your application. Thanks for reading and have good day :))