On this page
Providers, services, and dependency injection
What is a provider?
In NestJS, a provider is any class that can be injected as a dependency. Services, repositories, factories, helpers, and guards are all providers. The term comes from the fact that these classes "provide" functionality to other parts of the application.
The dependency injection (DI) system is one of NestJS's most powerful features. Instead of manually creating instances (new BooksService()), you declare your dependencies in constructors and let NestJS create and wire everything together. This makes code more modular, testable, and maintainable.
The @Injectable decorator
Any class you want to inject must be decorated with @Injectable(). This decorator marks the class as a provider and enables the DI container to manage its lifecycle.
@Injectable()
export class BooksService {
findAll(): Book[] {
return [];
}
}Without @Injectable(), NestJS cannot use the class as a provider and will throw an error when trying to resolve it.
Constructor injection
The standard way to inject dependencies is through the constructor:
@Injectable()
export class BooksService {
constructor(
private readonly authorsService: AuthorsService,
private readonly categoriesService: CategoriesService,
) {}
}NestJS reads the TypeScript types from the constructor parameters (thanks to emitDecoratorMetadata) and resolves the correct instances from the DI container. The private readonly pattern ensures the dependency cannot be reassigned and is only accessible within the class.
Provider scopes
By default, providers are singleton — a single instance is created and shared across the entire application. NestJS also supports two other scopes:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
// A new instance is created for every HTTP request
}
@Injectable({ Scope: Scope.TRANSIENT })
export class TransientService {
// A new instance is created every time it is injected
}Singleton (default) — Best for stateless services, database connections, and caches. Most services should be singletons.
Request — Useful when you need to store per-request data (like the current user). Note that request-scoped providers make their entire dependency chain request-scoped, which can hurt performance.
Transient — Each consumer gets its own instance. Useful for stateful utilities that should not share state.
Custom providers
When the standard class-based injection is not enough, NestJS provides several custom provider types.
Value providers
Inject a static value or an existing instance:
const CONFIG_PROVIDER = {
provide: 'APP_CONFIG',
useValue: { timeout: 5000, maxRetries: 3 },
};Class providers
Use a different class than the one specified by the token:
const BOOKS_REPO_PROVIDER = {
provide: BooksRepository,
useClass:
process.env['NODE_ENV'] === 'test'
? MockBooksRepository
: BooksRepository,
};This pattern is extremely useful for testing — swap real implementations with mocks at the module level.
Factory providers
Use a factory function to create the provider, with access to other injected dependencies:
const CACHE_PROVIDER = {
provide: 'CACHE_MANAGER',
useFactory: async (configService: ConfigService) => {
const ttl = configService.get<number>('CACHE_TTL') ?? 60;
return new CacheManager({ ttl });
},
inject: [ConfigService],
};Factory providers can be async, which is essential for database connections and other async initialization tasks.
Alias providers (useExisting)
Create an alias for an existing provider:
const ALIAS_PROVIDER = {
provide: 'BOOKS_SERVICE_ALIAS',
useExisting: BooksService,
};Both tokens resolve to the same singleton instance.
Exception handling in services
NestJS provides a set of built-in HTTP exceptions that translate to the correct HTTP status codes:
import {
NotFoundException, // 404
ConflictException, // 409
BadRequestException, // 400
UnauthorizedException, // 401
ForbiddenException, // 403
InternalServerErrorException, // 500
} from '@nestjs/common';
@Injectable()
export class BooksService {
findOne(id: number): Book {
const book = this.books.find((b) => b.id === id);
if (!book) throw new NotFoundException(`Book #${id} not found`);
return book;
}
}When a service throws an HttpException, NestJS's built-in exception filter catches it and sends the appropriate HTTP response with the correct status code and message. You don't need to handle exceptions in the controller.
Injecting non-class tokens
When you need to inject a provider registered with a string token, use the @Inject() decorator:
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class BooksService {
constructor(
@Inject('API_KEY') private readonly apiKey: string,
@Inject('CACHE_MANAGER') private readonly cache: CacheManager,
) {}
}Optional providers
Mark a dependency as optional with @Optional(). If the provider is not registered, undefined is injected instead of throwing an error:
import { Optional, Inject, Injectable } from '@nestjs/common';
@Injectable()
export class BooksService {
constructor(
@Optional() @Inject('METRICS') private readonly metrics?: MetricsService,
) {}
create(dto: CreateBookDto): Book {
this.metrics?.increment('books.created');
// ...
}
}The service as the single source of truth
A key architecture principle: the service layer owns the business logic. Controllers translate HTTP into function calls; services implement the actual rules.
@Injectable()
export class BooksService {
// All business rules live here:
// - What makes a book valid?
// - Can a book be deleted if it has active loans?
// - How is the price discounted for premium users?
}This means you can reuse the same service from multiple controllers, WebSocket gateways, CLI commands, or background jobs — the business rules are written once and tested independently from HTTP.
Lifecycle hooks
Services can implement lifecycle hooks to run code at specific moments:
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
@Injectable()
export class BooksService implements OnModuleInit, OnModuleDestroy {
async onModuleInit(): Promise<void> {
// Runs after the module is fully initialized
console.log('BooksService initialized');
}
async onModuleDestroy(): Promise<void> {
// Runs before the module is destroyed (e.g., app shutdown)
console.log('BooksService shutting down');
}
}Available hooks: OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, BeforeApplicationShutdown, OnApplicationShutdown.
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';
interface Book {
id: number;
title: string;
isbn: string;
price: number;
}
@Injectable()
export class BooksService {
private books: Book[] = [];
private nextId = 1;
findAll(): Book[] {
return this.books;
}
findOne(id: number): Book {
const book = this.books.find((b) => b.id === id);
if (!book) {
throw new NotFoundException(`Book #${id} not found`);
}
return book;
}
create(dto: CreateBookDto): Book {
const existing = this.books.find((b) => b.isbn === dto.isbn);
if (existing) {
throw new ConflictException(`ISBN ${dto.isbn} already exists`);
}
const book: Book = { id: this.nextId++, ...dto };
this.books.push(book);
return book;
}
update(id: number, dto: UpdateBookDto): Book {
const book = this.findOne(id);
Object.assign(book, dto);
return book;
}
remove(id: number): void {
const index = this.books.findIndex((b) => b.id === id);
if (index === -1) {
throw new NotFoundException(`Book #${id} not found`);
}
this.books.splice(index, 1);
}
}
Sign in to track your progress