Providers: Dependency Injection in NestJS

Understanding providers (services, repositories, factories, etc.), injecting dependencies, and the scope of providers.


NestJS: Providers & Repositories

Providers

In NestJS, a provider is a fundamental concept. It's essentially a class that can be injected into other classes, primarily via constructor injection. Providers can encapsulate various functionalities, such as services, factories, helpers, and repositories. They are declared using the @Injectable() decorator. This decorator marks the class as eligible for injection by the NestJS dependency injection system.

Key characteristics of providers:

  • Injectable: Marked with @Injectable().
  • Dependencies: Can themselves depend on other providers, allowing for complex dependency graphs.
  • Modular: Help break down an application into smaller, reusable, and testable modules.
  • Scopes: Can be scoped (e.g., REQUEST, TRANSIENT) to control their lifecycle.

Example: A Simple Service Provider

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

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

  findAll(): any[] {
    return this.users;
  }

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

Repositories

A repository is a specialized type of provider that encapsulates the logic for accessing and persisting data. It acts as an abstraction layer between your application's business logic and the underlying data source (e.g., a database, an API, a file system). This abstraction is crucial for several reasons.

Repositories are often used in conjunction with Object-Relational Mappers (ORMs) like TypeORM or Prisma, but they can also be used with other data access methods.

Repositories: Data Access Abstraction

Repositories provide an abstraction layer for data access and persistence. Here's how they improve code maintainability and testability:

  • Decoupling: Repositories decouple your application's business logic from the specific data access implementation. Your services don't need to know whether you're using a SQL database, a NoSQL database, or a mocked data source. They interact with the repository interface.
  • Testability: The decoupling provided by repositories makes testing much easier. You can easily mock or stub the repository in your unit tests, allowing you to isolate and test your service logic without having to interact with a real database.
  • Maintainability: If you need to change your data access technology (e.g., switch from MySQL to PostgreSQL or change ORMs), you only need to update the repository implementation. Your service code remains largely untouched, as long as the repository interface stays the same.
  • Code Reusability: Repositories encapsulate data access logic in a reusable component. This reduces code duplication and improves consistency across your application.
  • Centralized Data Access Logic: Repositories concentrate all data access operations in one place, making it easier to understand, debug, and optimize data interactions.

Example: Repository with TypeORM

 import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersRepository {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  async findOne(id: number): Promise<User | undefined> {
    return this.usersRepository.findOneBy({ id });
  }

  async create(user: Partial<User>): Promise<User> {
    return this.usersRepository.save(this.usersRepository.create(user));
  }

  async update(id: number, user: Partial<User>): Promise<User> {
    await this.usersRepository.update(id, user);
    return this.usersRepository.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.usersRepository.delete(id);
  }
} 

In this example, UsersRepository is responsible for all database interactions related to the User entity. The @InjectRepository(User) decorator injects the TypeORM repository for the User entity. The methods in the repository then use this injected repository to perform CRUD (Create, Read, Update, Delete) operations.

Using Providers and Repositories Together

Services often depend on repositories. A service orchestrates business logic, and when that logic requires data access, it delegates to the repository.

Example: Service using Repository

 import { Injectable } from '@nestjs/common';
import { UsersRepository } from './users.repository';

@Injectable()
export class UsersService {
  constructor(private usersRepository: UsersRepository) {}

  async getAllUsers() {
    return this.usersRepository.findAll();
  }

  async getUserById(id: number) {
    return this.usersRepository.findOne(id);
  }

  async createUser(userData: any) {
    return this.usersRepository.create(userData);
  }
} 

Here, the UsersService uses the UsersRepository to retrieve, create, and manage user data. The service focuses on business logic, while the repository handles the data access details.