NestJS in Production · Structure, Validation and Errors
Modules, Providers and Dependency Injection
Organise a NestJS service into feature modules, inject providers through constructors, and swap implementations with custom providers.
What you'll build
An orders service split into two modules: OrdersModule (controller and service) depends on PaymentsModule, which exports a PaymentsService. A custom provider token lets you swap the payment gateway implementation without touching the orders code.
The idea
- A module groups controllers and providers for one feature; it must export providers that other modules use, and import the modules it depends on.
- Providers (services, repositories, clients) are created by Nest's injector and passed to constructors, so classes don't build their own dependencies.
- Custom providers (
{ provide: TOKEN, useClass | useValue | useFactory }) bind an interface-like token to an implementation; inject them with@Inject(TOKEN). - Everything is a singleton by default: one instance per application.
The code
import "reflect-metadata";
import { Body, Controller, Inject, Injectable, Module, Post } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
interface PaymentGateway { charge(cents: number): Promise<string>; }
const PAYMENT_GATEWAY = Symbol("PAYMENT_GATEWAY");
@Injectable()
class FakeGateway implements PaymentGateway {
async charge(cents: number) { return `fake-charge-${cents}`; }
}
@Injectable()
class PaymentsService {
constructor(@Inject(PAYMENT_GATEWAY) private readonly gateway: PaymentGateway) {}
pay(cents: number) { return this.gateway.charge(cents); }
}
@Module({ providers: [PaymentsService, { provide: PAYMENT_GATEWAY, useClass: FakeGateway }], exports: [PaymentsService] })
class PaymentsModule {}
@Injectable()
class OrdersService {
private nextId = 1;
constructor(private readonly payments: PaymentsService) {}
async place(items: number, unitCents: number) {
const chargeId = await this.payments.pay(items * unitCents);
return { id: this.nextId++, total: items * unitCents, chargeId };
}
}
@Controller("orders")
class OrdersController {
constructor(private readonly orders: OrdersService) {}
@Post() place(@Body() body: { items: number; unitCents: number }) { return this.orders.place(body.items, body.unitCents); }
}
@Module({ imports: [PaymentsModule], controllers: [OrdersController], providers: [OrdersService] })
class OrdersModule {}
@Module({ imports: [OrdersModule] })
class AppModule {}
async function main() {
const app = await NestFactory.create(AppModule, { logger: false });
await app.listen(0, "127.0.0.1");
const base = `http://127.0.0.1:${app.getHttpServer().address().port}`;
const r = await fetch(`${base}/orders`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: 3, unitCents: 1250 }) });
console.log(r.status, JSON.stringify(await r.json()));
console.log("same OrdersService instance everywhere:", app.get(OrdersService) === app.get(OrdersService));
await app.close();
}
main();
Output
201 {"id":1,"total":3750,"chargeId":"fake-charge-3750"}
same OrdersService instance everywhere: trueRun this on your computer — see “Get Started” for setup.
Common mistakes
- Using a provider from another module without that module exporting it ("Nest can't resolve dependencies").
- Circular imports between modules; extract the shared part into a third module (or, as a last resort,
forwardRef). - Creating dependencies with
newinside services, which defeats testing and configuration. - Interfaces as injection tokens: TypeScript interfaces don't exist at runtime, so use a symbol or string token.
Key points
- Modules group a feature and export what others may use.
- Constructor injection with custom provider tokens keeps implementations swappable.
- Providers are singletons unless you choose another scope.
Further reading: Modules · Custom providers
Practice questions
What causes 'Nest can't resolve dependencies of X'?
Answer
A constructor dependency isn't available in the module's scope: its provider isn't registered there, or it lives in another module that isn't imported or doesn't export it.
Why can't a TypeScript interface be an injection token?
Answer
Interfaces are erased at compile time, so they don't exist at runtime; use a symbol or string token with @Inject(TOKEN).
What is the default scope of a NestJS provider?
Answer
Singleton: one instance shared across the whole application.
Open in the app to mark lessons complete, take notes and track your progress.