NestJS Providers: The Core Building Blocks of Dependency Injection

NestJS Providers: The Core Building Blocks of Dependency Injection

Discover all you need to know about NestJS providers

NestJS is a powerful framework for building scalable and maintainable server-side applications. One of its key features is the robust dependency injection system, which revolves around the concept of providers. In this blog post, we'll dive deep into what providers are, how they work, and how you can use them to create clean, modular, and testable code in your NestJS applications.

What are Providers?

In NestJS, providers are the basic building blocks for dependency injection. They are responsible for creating and managing the instances of the classes they provide. Providers can be services, repositories, factories, or any other class that holds business logic or data access logic.

Types of Providers

NestJS supports various types of providers, each serving a specific purpose:

  1. Class Providers: The most common type, where a class is used as a provider.

  2. Value Providers: Provide a static value, useful for constants or configurations.

  3. Factory Providers: Use a factory function to create the provider's value.

  4. Existing Providers: Alias an existing provider, useful for creating different names for the same provider.

Class Providers

Class providers are the most straightforward and commonly used providers in NestJS. They are simply classes decorated with the @Injectable() decorator.

Example: UserService

Let's create a simple UserService that provides user-related functionalities.

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

@Injectable()
export class UserService {
  private users = [
    { id: 1, name: 'John Doe' },
    { id: 2, name: 'Jane Doe' },
  ];

  getUsers() {
    return this.users;
  }

  getUserById(id: number) {
    return this.users.find(user => user.id === id);
  }

  createUser(name: string) {
    const newUser = { id: Date.now(), name };
    this.users.push(newUser);
    return newUser;
  }
}

Usage in a Module

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
  providers: [UserService],
  controllers: [UserController],
  exports: [UserService],
})
export class UserModule {}

Injecting into a Controller

import { Controller, Get, Param, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  getAllUsers() {
    return this.userService.getUsers();
  }

  @Get(':id')
  getUser(@Param('id') id: number) {
    return this.userService.getUserById(id);
  }

  @Post()
  createUser(@Body('name') name: string) {
    return this.userService.createUser(name);
  }
}

Value Providers

Value providers are useful when you need to provide a constant value. This is often used for configurations.

Example: Configuration

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

const CONFIG = {
  database: 'my_database',
  apiKey: '123456789',
};

@Module({
  providers: [
    {
      provide: 'CONFIG',
      useValue: CONFIG,
    },
  ],
  exports: ['CONFIG'],
})
export class ConfigModule {}

Here, CONFIG is provided as a constant value that can be injected wherever needed.

Usage in a Service

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

@Injectable()
export class AppService {
  constructor(@Inject('CONFIG') private config: any) {}

  getDatabaseName(): string {
    return this.config.database;
  }
}

Factory Providers

Factory providers allow you to create a value dynamically using a factory function. This is useful when the value needs to be computed or when dependencies are required to create the value.

Example: Database Connection

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

@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async () => {
        const connection = await createDatabaseConnection();
        return connection;
      },
    },
  ],
  exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}

async function createDatabaseConnection() {
  // Simulating database connection creation
  return {
    connect: () => console.log('Connected to database'),
    disconnect: () => console.log('Disconnected from database'),
  };
}

In this example, the DATABASE_CONNECTION is created dynamically using a factory function.

Usage in a Service

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

@Injectable()
export class AppService {
  constructor(@Inject('DATABASE_CONNECTION') private dbConnection: any) {}

  connectToDatabase() {
    this.dbConnection.connect();
  }

  disconnectFromDatabase() {
    this.dbConnection.disconnect();
  }
}

Existing Providers

Existing providers allow you to alias an existing provider. This is useful when you want to provide the same instance under different tokens.

Example: Alias for Logging Service

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

@Injectable()
export class LoggingService {
  log(message: string) {
    console.log(message);
  }
}

@Module({
  providers: [
    LoggingService,
    {
      provide: 'Logger',
      useExisting: LoggingService,
    },
  ],
  exports: ['Logger'],
})
export class LoggingModule {}

Usage in a Service

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

@Injectable()
export class AppService {
  constructor(@Inject('Logger') private logger: any) {}

  performAction() {
    this.logger.log('Action performed');
  }
}

Using Providers in Modules

To use a provider, you need to register it in a module. Modules in NestJS act as organizational units that group related providers, controllers, and other modules together.

Example: App Module

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

@Injectable()
export class LoggingService {
  log(message: string) {
    console.log(message);
  }
}

@Module({
  providers: [
    LoggingService,
    {
      provide: 'Logger',
      useExisting: LoggingService,
    },
  ],
  exports: ['Logger'],
})
export class LoggingModule {}

Usage in a Service

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

@Injectable()
export class AppService {
  constructor(@Inject('Logger') private logger: any) {}

  performAction() {
    this.logger.log('Action performed');
  }
}

Putting all things together

Combining these examples, let's see how they can be structured within modules and used together.

App Module

import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
import { ConfigModule } from './config/config.module';
import { DatabaseModule } from './database/database.module';
import { LoggingModule } from './logging/logging.module';
import { AppService } from './app.service';
import { AppController } from './app.controller';

@Module({
  imports: [UserModule, ConfigModule, DatabaseModule, LoggingModule],
  providers: [AppService],
  controllers: [AppController],
})
export class AppModule {}

App Service

import { Injectable, Inject } from '@nestjs/common';
import { UserService } from './user/user.service';

@Injectable()
export class AppService {
  constructor(
    private readonly userService: UserService,
    @Inject('CONFIG') private config: any,
    @Inject('DATABASE_CONNECTION') private dbConnection: any,
    @Inject('Logger') private logger: any,
  ) {}

  performTask() {
    this.logger.log('Performing task...');
    const users = this.userService.getUsers();
    this.dbConnection.connect();
    console.log(`Database: ${this.config.database}`);
    this.dbConnection.disconnect();
    return users;
  }
}

App Controller

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getData() {
    return this.appService.performTask();
  }
}

By using these various types of providers and organizing them into modules, you can create a highly modular and maintainable NestJS application. Each type of provider serves a specific purpose, allowing you to manage dependencies effectively and inject them wherever needed in your application.

Conclusion
Providers are the backbone of NestJS's powerful dependency injection system. By understanding the different types of providers and how to use them effectively, you can create scalable, maintainable, and testable applications. Whether you're building services, repositories, or dynamic configurations, NestJS providers give you the flexibility and control you need to manage your application's dependencies seamlessly.

Happy coding with NestJS!