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.

Create a free account to save this page, take notes and track your progress. Sign in

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

Two feature modules and a swappable gateway
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: true

Run 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 new inside 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.

Was this page helpful?