providers

SKILL.md

NestJS Providers

When to Use This Skill

Use this skill when:

  • Creating services for business logic
  • Implementing dependency injection
  • Working with repositories or data access layers
  • Creating custom providers with factories or values
  • Understanding provider scopes and lifecycles
  • Injecting dependencies into controllers or other providers

What are Providers?

Providers are classes that can be injected as dependencies. They handle business logic, data access, and can be shared across the application through NestJS's IoC (Inversion of Control) container.

Basic Service

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

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

  create(cat: Cat) {
    this.cats.push(cat);
  }

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

  findOne(id: string): Cat {
    return this.cats.find(cat => cat.id === id);
  }
}

Key Points:

  • @Injectable() decorator marks class as provider
  • Enables dependency injection
  • Contains business logic separate from controllers
  • Can be injected into other classes

Dependency Injection

Constructor Injection (Recommended)

import { Controller } from '@nestjs/common';
import { CatsService } from './cats.service';

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

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

Property Injection

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

@Injectable()
export class CatsService {
  @Inject('CONNECTION')
  private connection: Connection;
}

Use property injection only when:

  • The class extends another class with constructor params
  • You need to inject into a base class

Registering Providers

Providers must be registered in a module:

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

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

Provider Patterns

Standard Provider (Class)

@Module({
  providers: [CatsService], // Shorthand
})

// Equivalent to:
@Module({
  providers: [
    {
      provide: CatsService,
      useClass: CatsService,
    },
  ],
})

Value Provider

Inject a constant value or mock object:

const mockCatsService = {
  findAll: () => [],
};

@Module({
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})

Use cases:

  • Testing (mocking dependencies)
  • Injecting configuration objects
  • External libraries

Class Provider (Alternative Implementation)

class ConfigService {
  // Base implementation
}

class DevelopmentConfigService extends ConfigService {
  // Development-specific logic
}

@Module({
  providers: [
    {
      provide: ConfigService,
      useClass: DevelopmentConfigService,
    },
  ],
})

Factory Provider

Create provider dynamically based on other dependencies:

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useFactory: async (config: ConfigService) => {
        const connection = await createConnection(config.getDatabaseConfig());
        return connection;
      },
      inject: [ConfigService],
    },
  ],
})

Key Points:

  • useFactory - Function that returns the provider value
  • inject - Array of dependencies to inject into factory
  • Can be async
  • Perfect for conditional logic or async initialization

Alias Provider (Existing)

Create an alias for an existing provider:

@Module({
  providers: [
    ConfigService,
    {
      provide: 'AliasedConfigService',
      useExisting: ConfigService,
    },
  ],
})

Custom Provider Tokens

String Tokens

@Module({
  providers: [
    {
      provide: 'API_KEY',
      useValue: 'my-secret-key',
    },
  ],
})

// Inject with @Inject()
@Injectable()
export class ApiService {
  constructor(@Inject('API_KEY') private apiKey: string) {}
}

Symbol Tokens

export const API_KEY = Symbol('API_KEY');

@Module({
  providers: [
    {
      provide: API_KEY,
      useValue: 'my-secret-key',
    },
  ],
})

@Injectable()
export class ApiService {
  constructor(@Inject(API_KEY) private apiKey: string) {}
}

Optional Dependencies

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

@Injectable()
export class HttpService {
  constructor(
    @Optional()
    @Inject('HTTP_OPTIONS')
    private httpOptions: HttpOptions
  ) {
    if (!httpOptions) {
      this.httpOptions = defaultOptions;
    }
  }
}

Provider Scopes

Default (Singleton)

Provider instance shared across entire application:

@Injectable()
export class CatsService {}

Request Scope

New instance created for each request:

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

@Injectable({ scope: Scope.REQUEST })
export class CatsService {}

Use when:

  • Need request-specific data
  • Multi-tenancy per request
  • Request tracking

Performance note: Request-scoped providers have performance implications

Transient Scope

New instance created each time it's injected:

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

@Injectable({ scope: Scope.TRANSIENT })
export class CatsService {}

Exporting Providers

Make providers available to other modules:

@Module({
  providers: [CatsService],
  exports: [CatsService], // Now available to importing modules
})
export class CatsModule {}

// In another module
@Module({
  imports: [CatsModule],
  controllers: [DogsController], // Can inject CatsService
})
export class DogsModule {}

Async Providers

For providers requiring async initialization:

@Module({
  providers: [
    {
      provide: 'ASYNC_CONNECTION',
      useFactory: async () => {
        const connection = await createConnection();
        return connection;
      },
    },
  ],
})

Complete Example

// database.providers.ts
export const databaseProviders = [
  {
    provide: 'DATABASE_CONNECTION',
    useFactory: async (config: ConfigService) => {
      const connection = await createConnection({
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
      });
      return connection;
    },
    inject: [ConfigService],
  },
];

// cats.repository.ts
@Injectable()
export class CatsRepository {
  constructor(
    @Inject('DATABASE_CONNECTION')
    private connection: Connection,
  ) {}

  async findAll(): Promise<Cat[]> {
    return this.connection.query('SELECT * FROM cats');
  }

  async create(cat: CreateCatDto): Promise<Cat> {
    return this.connection.insert('cats', cat);
  }
}

// cats.service.ts
@Injectable()
export class CatsService {
  constructor(
    private readonly catsRepository: CatsRepository,
    private readonly logger: Logger,
  ) {}

  async findAll(): Promise<Cat[]> {
    this.logger.log('Finding all cats');
    return this.catsRepository.findAll();
  }

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    this.logger.log('Creating a cat');
    return this.catsRepository.create(createCatDto);
  }
}

// cats.module.ts
@Module({
  providers: [
    ...databaseProviders,
    CatsRepository,
    CatsService,
    Logger,
  ],
  controllers: [CatsController],
  exports: [CatsService], // Available to other modules
})
export class CatsModule {}

Circular Dependencies

Use forwardRef() to resolve circular dependencies:

// cats.service.ts
@Injectable()
export class CatsService {
  constructor(
    @Inject(forwardRef(() => DogsService))
    private dogsService: DogsService,
  ) {}
}

// dogs.service.ts
@Injectable()
export class DogsService {
  constructor(
    @Inject(forwardRef(() => CatsService))
    private catsService: CatsService,
  ) {}
}

Best Practices

  1. Single Responsibility - Each service should have one clear purpose
  2. Use constructor injection - Cleaner than property injection
  3. Prefer singleton scope - Unless you specifically need request/transient
  4. Export selectively - Only export what other modules need
  5. Use interfaces - Define contracts for better testing
  6. Avoid circular dependencies - Refactor to a shared service if needed
  7. Use factories for complex initialization - Especially with async operations
  8. Type your providers - Leverage TypeScript for type safety
  9. Keep services testable - Inject all dependencies
  10. Use descriptive names - UserService, AuthService, not Service1

Common Patterns

Repository Pattern

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

  async findById(id: string): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }
}

Service Layer Pattern

@Injectable()
export class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly emailService: EmailService,
  ) {}

  async createUser(dto: CreateUserDto): Promise<User> {
    const user = await this.userRepository.create(dto);
    await this.emailService.sendWelcomeEmail(user.email);
    return user;
  }
}

Factory Pattern

@Injectable()
export class CatFactory {
  create(type: string): Cat {
    switch (type) {
      case 'persian':
        return new PersianCat();
      case 'siamese':
        return new SiameseCat();
      default:
        return new Cat();
    }
  }
}

Testing Providers

describe('CatsService', () => {
  let service: CatsService;
  let repository: CatsRepository;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CatsService,
        {
          provide: CatsRepository,
          useValue: {
            findAll: jest.fn(),
            create: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<CatsService>(CatsService);
    repository = module.get<CatsRepository>(CatsRepository);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});
Weekly Installs
1
GitHub Stars
1
First Seen
6 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
warp1