Providers & Dependency Injection
Understand providers and dependency injection in NestJS: services, constructor injection, singleton scope, custom providers and injection tokens.
On this page 12 sections
Introduction
Providers are where the real work of a Nest application happens — business rules, database access, calls to other APIs. Dependency injection (DI) is the mechanism Nest uses to create those providers and hand them to whichever classes need them. Together they are the single most important concept in NestJS.
- What dependency injection is and why it matters.
- How to create a service with @Injectable().
- How constructor injection works.
- Provider scopes: singleton, request and transient.
- Custom providers with useValue, useClass and useFactory.
What is Dependency Injection?
Without DI, a class creates its own dependencies:
// Without DI — tightly coupledclass ProductsController { private productsService = new ProductsService(new Database('prod-url'));}This controller is now welded to one specific service and one specific database. You cannot swap in a fake database for testing, and if ProductsService's constructor changes, every class that creates it breaks.
With DI, a class simply declares what it needs, and something else (the "injector" or "container") creates and supplies it. In Nest, that container is built for you automatically from your modules.
// With DI — loosely coupled@Controller('products')class ProductsController { constructor(private readonly productsService: ProductsService) {}}Creating a Service
// src/products/products.service.tsimport { Injectable, NotFoundException } from '@nestjs/common';import { CreateProductDto } from './dto/create-product.dto.js';
export interface Product { id: number; name: string; price: number;}
@Injectable()export class ProductsService { private products: Product[] = []; private nextId = 1;
create(dto: CreateProductDto): Product { const product = { id: this.nextId++, ...dto }; this.products.push(product); return product; }
findAll(): Product[] { return this.products; }
findOne(id: number): Product { const product = this.products.find((p) => p.id === id); if (!product) { throw new NotFoundException(`Product ${id} not found`); } return product; }
remove(id: number): void { this.findOne(id); // throws if missing this.products = this.products.filter((p) => p.id !== id); }}@Injectable() marks the class as something Nest's container can manage. The service is then registered in its module's providers array — the CLI did this for you in lesson 3.
Injecting a Service
To use the service, declare it as a constructor parameter. The private readonly shorthand both declares and assigns the property in one step:
@Controller('products')export class ProductsController { constructor(private readonly productsService: ProductsService) {}
@Get() findAll() { return this.productsService.findAll(); }}Services can inject other services exactly the same way — a ProductsService might inject a PricingService, which might inject a CurrencyService. Nest works out the correct creation order for the whole chain.
How Nest Resolves Dependencies
- At startup, Nest reads the type of each constructor parameter (TypeScript emits this metadata because of the decorators).
- It looks for a provider registered under that type in the current module or an imported module's exports.
- If an instance already exists, it reuses it; otherwise it creates one — first resolving that provider's own dependencies.
- It passes the instances into the constructor.
"Nest can't resolve dependencies of the ProductsController (?). Please make sure that the argument ProductsService at index [0] is available in the ProductsModule context." — the "?" marks the missing dependency, and the fix is always to make that provider available in the named module.
Provider Scopes
| Scope | Behavior | When to Use |
|---|---|---|
| DEFAULT (singleton) | One instance shared across the whole application. | Almost always — it is the fastest and the default. |
| REQUEST | A new instance for every incoming request. | When a provider must hold per-request state, like the current tenant. |
| TRANSIENT | A new instance for every class that injects it. | For lightweight helpers that must not share state, like a per-class logger. |
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })export class RequestContextService {}A request-scoped provider makes every class that depends on it request-scoped too, which has a performance cost. Stick to the default singleton scope unless you have a specific reason.
Custom Providers
Listing a class in providers is shorthand for { provide: ProductsService, useClass: ProductsService }. The long form lets you control exactly what gets injected:
@Module({ providers: [ // useValue — inject a fixed value or object { provide: 'APP_NAME', useValue: 'PrograMinds Store' },
// useClass — choose an implementation { provide: PaymentService, useClass: process.env.NODE_ENV === 'test' ? FakePaymentService : StripePaymentService, },
// useFactory — build the value with code (can be async) { provide: 'TAX_RATE', useFactory: () => Number(process.env.TAX_RATE ?? 0.18), }, ],})export class ProductsModule {}Providers registered with a string (or Symbol) token are injected with @Inject():
@Injectable()export class PricingService { constructor(@Inject('TAX_RATE') private readonly taxRate: number) {}
withTax(price: number) { return price * (1 + this.taxRate); }}Common Mistakes
- Forgetting @Injectable() on a service that itself has constructor dependencies.
- Creating dependencies with new inside a class instead of injecting them.
- Using import type for a class you inject — type-only imports are erased, so Nest cannot see the dependency at runtime.
- Making providers request-scoped "just in case", slowing down the whole dependency chain.
Best Practices
- Put all business logic in services; keep controllers as thin adapters over HTTP.
- Inject dependencies through the constructor with private readonly.
- Keep the default singleton scope unless you truly need per-request state.
- Use custom providers (useClass/useValue) to swap real implementations for fakes in tests.
Frequently Asked Questions
A service is the most common kind of provider. "Provider" is the general term for anything Nest can inject — services, repositories, factories, helpers or plain values.
Plain values (numbers, strings, config objects) have no class to use as a token, so you give them a name. For larger apps, exporting a constant or Symbol avoids typos.
No measurable amount for singleton providers — the container does its work once at startup and then simply hands out existing instances.
Key Takeaways
- Providers hold business logic; @Injectable() lets Nest manage them.
- Dependency injection means classes declare what they need instead of creating it.
- Nest injects dependencies through constructor parameters based on their types.
- Providers are singletons by default; REQUEST and TRANSIENT scopes exist for special cases.
- Custom providers (useValue, useClass, useFactory) control exactly what gets injected.
Summary
You now understand the heart of NestJS: services that hold your logic, and dependency injection that connects everything without tight coupling. Next, you will make sure only valid data ever reaches those services, using DTOs, validation and pipes.