Skip to content

NestJS modules, custom providers and why DI makes testing cheap

Consumers ask by token; the root decides what answers, and a test overrides that token, not the class.

Predict first

Before you read: predict

ExamHall's grading tests must not call the ID-check vendor, so the grading spec replaces its client:

The spec calls run() once.

How many times does Nest call VendorIdCheckClient's constructor, and what does run() return?

Code for this question
// vendor/vendor-id-check.client.tsimport { Injectable } from '@nestjs/common'; @Injectable()export class VendorIdCheckClient {  check(candidateId: string) { return `vendor checked ${candidateId}`; }} // proctoring/proctoring.module.tsimport { Injectable, Module } from '@nestjs/common';import { VendorIdCheckClient } from '../vendor/vendor-id-check.client.js'; @Injectable()export class FlagService {  constructor(private idCheck: VendorIdCheckClient) {}  flag(candidateId: string) { return this.idCheck.check(candidateId); }} @Module({ providers: [FlagService, VendorIdCheckClient], exports: [FlagService] })export class ProctoringModule {} // grading/grading.module.tsimport { Injectable, Module } from '@nestjs/common';import { FlagService, ProctoringModule } from '../proctoring/proctoring.module.js'; @Injectable()export class GradingService {  constructor(private flags: FlagService) {}  review(candidateId: string) { return this.flags.flag(candidateId); }} @Module({ imports: [ProctoringModule], providers: [GradingService] })export class GradingModule {} // grading/grading.spec.tsimport { Test } from '@nestjs/testing';import { GradingModule, GradingService } from './grading.module.js';import { VendorIdCheckClient } from '../vendor/vendor-id-check.client.js'; export async function run() {  const moduleRef = await Test.createTestingModule({ imports: [GradingModule] })    .overrideProvider(VendorIdCheckClient)    .useValue({ check: (candidateId: string) => `fake checked ${candidateId}` })    .compile();  return moduleRef.get(GradingService).review('c-7');}
Pick one

The 30-second version

A class only lists what it needs; the module registering providers, the composition root, decides what fills each token: a class Nest builds, an existing value, a factory's result or an alias. A test builds the real graph and overrides one token, so one boundary is swapped and nothing else changes.

Consumers ask by token; the root decides what answers, and a test overrides that token, not the class.

How it works

Step 1 of 15

The root chooses. A class lists its needs in its constructor; the module registering the providers, the composition root, decides which implementation fills each token.
In the kitchen
  • the running order names looks, never models; the show's producer books who walks each look → a constructor names tokens; the composition root decides what fills them (step 1)

Analogy: A catwalk show's running order (fashion shows)

A designer's show is planned on a running order: "look 4: the green coat".

At the show In NestJS
the running order names looks, never models; the show's producer books who walks each look a constructor names tokens; the composition root decides what fills them (step 1)
a model booked through an agency, a coat already finished on the rail, a garment the atelier makes from that morning's fabric useClass, useValue, useFactory (steps 2–4)
"look 9 is whoever walks look 4": the same model, not a second one useExisting, an alias (step 5)
at the dress rehearsal the producer puts a fit model on look 4, and every list that says "look 4" follows overrideProvider on the token, everywhere in the graph (steps 7–8)
two looks that each must come out straight after the other cannot both open the show a cycle (steps 10–12)
the same collection staged in two cities, each with its own venue settings two forRoot calls, two module instances (steps 13–14)

In real terms: the token is the look number; the registration is the booking; the test changes the booking, not the running order.

Breaks down when: a producer can rebook one model mid-show. Nest builds the graph once, at startup or at compile(); an override must be set before compile().

Contrasting case: a designer who sews and walks every look herself has nothing to book; a script that builds its two objects in main.ts needs no container at all (pat-dip-di-nestjs step 4).

Examples, run for real

4.1 ExamHall EXH-3730: a vendor client built inside FlagService

ProctoringModule exports FlagService, and GradingModule imports it. FlagService made its own client with new:

@Injectable()
export class FlagService {
  private idCheck = new VendorIdCheckClient();
  flag(candidateId: string) { return this.idCheck.check(candidateId); }
}
4.1 EXH-3730 FlagService calls new VendorIdCheckClient(), spec overrides VendorIdCheckClient: vendor client built 1 time(s) · run() returns 'vendor checked c-7'
4.1 MR !4030 client registered in ProctoringModule and injected, same spec: vendor client built 0 time(s) · run() returns 'fake checked c-7'

The grading spec overrode VendorIdCheckClient, but no registration provided that token, so the override replaced nothing and the vendor was called. MR !4030 registers the client in ProctoringModule and injects it through the constructor; the same spec's override now reaches it (steps 1, 7 and 15).

4.2 ExamHall EXH-3731: two services that inject each other

To show a flag's effect on the score, GradingService injected FlagService and FlagService injected GradingService back, each in its own file, both in GradingModule:

4.2 EXH-3731 GradingService and FlagService inject each other (two files, one module): startup throws UnknownDependenciesException
4.2 MR !4031 shared rule in PenaltyRules, both inject it: starts · GradingService 1 · FlagService 1 · PenaltyRules 1 · score(71, c-7) 61 · cost(c-7) 10

MR !4031 moves the shared rule, penaltyFor(flags), into PenaltyRules, which both services inject; neither injects the other (steps 10 and 12).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 6

A box office's NestJS app prices seats in four services:

The app starts.

How many times has Nest called SeatPricer's constructor?

Code for this question
import { Inject, Injectable, Module } from '@nestjs/common'; export const PRICER = Symbol('PRICER'); @Injectable()export class SeatPricer {  price(seat: string) { return seat.startsWith('A') ? 9000 : 4500; }} @Injectable()export class BookingService { constructor(private pricer: SeatPricer) {} } @Injectable()export class RefundService { constructor(private pricer: SeatPricer) {} } @Injectable()export class QuoteService { constructor(@Inject(PRICER) private pricer: SeatPricer) {} } @Injectable()export class HoldService { constructor(@Inject('LEGACY_PRICER') private pricer: SeatPricer) {} } @Module({  providers: [    SeatPricer,    { provide: PRICER, useClass: SeatPricer },    { provide: 'LEGACY_PRICER', useExisting: SeatPricer },    BookingService, RefundService, QuoteService, HoldService,  ],})export class AppModule {}
Pick one

Question 2 of 6

A ticketing app configures its seat map per venue with a dynamic module:

The app starts and asks each service for its venue.

What does it get?

Code for this question
// seat-map/seat-map.module.tsimport { DynamicModule, Inject, Injectable, Module } from '@nestjs/common'; export const SEAT_MAP_OPTIONS = 'SEAT_MAP_OPTIONS'; @Injectable()export class SeatMapService {  constructor(@Inject(SEAT_MAP_OPTIONS) private options: { venue: string }) {}  venue() { return this.options.venue; }} @Module({})export class SeatMapModule {  static forRoot(options: { venue: string }): DynamicModule {    return {      module: SeatMapModule,      providers: [{ provide: SEAT_MAP_OPTIONS, useValue: options }, SeatMapService],      exports: [SeatMapService],    };  }} // resale/resale.module.tsimport { Injectable, Module } from '@nestjs/common';import { SeatMapModule, SeatMapService } from '../seat-map/seat-map.module.js'; @Injectable()export class ResaleService {  constructor(private seats: SeatMapService) {}  venue() { return this.seats.venue(); }} @Module({ imports: [SeatMapModule.forRoot({ venue: 'theatre' })], providers: [ResaleService], exports: [ResaleService] })export class ResaleModule {} // app.module.tsimport { Injectable, Module } from '@nestjs/common';import { SeatMapModule, SeatMapService } from './seat-map/seat-map.module.js';import { ResaleModule } from './resale/resale.module.js'; @Injectable()export class BoxOfficeService {  constructor(private seats: SeatMapService) {}  venue() { return this.seats.venue(); }} @Module({ imports: [SeatMapModule.forRoot({ venue: 'arena' }), ResaleModule], providers: [BoxOfficeService] })export class AppModule {}
Pick one

Question 3 of 6

A box office splits seats and pricing into two modules. A held seat's price needs the hold fee, and a hold needs the seat's price:

Startup fails.

Which one change lets the app start, with SeatHoldService and PricingService each built once?

Code for this question
// seats/seats.module.tsimport { Injectable, Module } from '@nestjs/common';import { PricingModule, PricingService } from '../pricing/pricing.module.js'; @Injectable()export class SeatHoldService {  constructor(private pricing: PricingService) {}  holdFee() { return 150; }  hold(seat: string) { return this.pricing.price(seat) + this.holdFee(); }} @Module({ imports: [PricingModule], providers: [SeatHoldService], exports: [SeatHoldService] })export class SeatsModule {} // pricing/pricing.module.tsimport { Injectable, Module } from '@nestjs/common';import { SeatHoldService, SeatsModule } from '../seats/seats.module.js'; @Injectable()export class PricingService {  constructor(private holds: SeatHoldService) {}  price(seat: string) { return (seat.startsWith('A') ? 9000 : 4500) + this.holds.holdFee(); }} @Module({ imports: [SeatsModule], providers: [PricingService], exports: [PricingService] })export class PricingModule {} // app.module.tsimport { Module } from '@nestjs/common';import { SeatsModule } from './seats/seats.module.js'; @Module({ imports: [SeatsModule] })export class AppModule {}
Pick one

Question 4 of 6

A box office's seat-hold tests must not lock real seats, so the checkout spec gives its testing module a fake seat-lock client:

The spec calls run() once.

How many times does Nest call SeatLockClient's constructor, and what does run() return?

Code for this question
// venue/seat-lock.client.tsimport { Injectable } from '@nestjs/common'; @Injectable()export class SeatLockClient {  lock(seat: string) { return `hall locked ${seat}`; }} // checkout/checkout.module.tsimport { Injectable, Module } from '@nestjs/common';import { SeatLockClient } from '../venue/seat-lock.client.js'; @Injectable()export class CheckoutService {  constructor(private locks: SeatLockClient) {}  hold(seat: string) { return this.locks.lock(seat); }} @Module({ providers: [CheckoutService, SeatLockClient], exports: [CheckoutService] })export class CheckoutModule {} // checkout/checkout.spec.tsimport { Test } from '@nestjs/testing';import { CheckoutModule, CheckoutService } from './checkout.module.js';import { SeatLockClient } from '../venue/seat-lock.client.js'; export async function run() {  const moduleRef = await Test.createTestingModule({    imports: [CheckoutModule],    providers: [{ provide: SeatLockClient, useValue: { lock: (seat: string) => `fake locked ${seat}` } }],  }).compile();  return moduleRef.get(CheckoutService).hold('B12');}
Pick one

Question 5 of 6

A chat app binds its push client under a symbol, and the message spec replaces it:

The spec calls run() once.

How many times does Nest call FcmPushClient's constructor, and what does run() return?

Code for this question
// push/push.client.tsimport { Injectable } from '@nestjs/common'; export const PUSH = Symbol('PUSH'); @Injectable()export class FcmPushClient {  send(userId: string) { return `fcm sent to ${userId}`; }} // chat/chat.module.tsimport { Inject, Injectable, Module } from '@nestjs/common';import { FcmPushClient, PUSH } from '../push/push.client.js'; @Injectable()export class MessageService {  constructor(@Inject(PUSH) private push: FcmPushClient) {}  post(userId: string) { return this.push.send(userId); }} @Module({ providers: [MessageService, { provide: PUSH, useClass: FcmPushClient }], exports: [MessageService] })export class ChatModule {} // chat/message.spec.tsimport { Test } from '@nestjs/testing';import { ChatModule, MessageService } from './chat.module.js';import { FcmPushClient } from '../push/push.client.js'; export async function run() {  const moduleRef = await Test.createTestingModule({ imports: [ChatModule] })    .overrideProvider(FcmPushClient)    .useValue({ send: (userId: string) => `fake sent to ${userId}` })    .compile();  return moduleRef.get(MessageService).post('u-1');}
Pick one

Question 6 of 6

A ledger's NestJS app picks its card-charges client in a factory. The app runs in production:

The app starts.

Which class does CheckoutService hold, and how many times did chargesFor run?

Code for this question
import { Inject, Injectable, Module } from '@nestjs/common'; export const CHARGES = 'CHARGES'; @Injectable()export class EnvConfig { stage = 'production'; } @Injectable()export class FeatureFlags { stage = 'beta'; sandboxCheckout = false; } export class LiveCharges { name = 'LiveCharges'; }export class SandboxCharges { name = 'SandboxCharges'; } export function chargesFor(env: EnvConfig, flags: FeatureFlags) {  return env.stage === 'production' && !flags.sandboxCheckout ? new LiveCharges() : new SandboxCharges();} @Injectable()export class CheckoutService { constructor(@Inject(CHARGES) public charges: LiveCharges | SandboxCharges) {} } @Injectable()export class RefundService { constructor(@Inject(CHARGES) public charges: LiveCharges | SandboxCharges) {} } @Module({  providers: [    EnvConfig, FeatureFlags,    { provide: CHARGES, useFactory: chargesFor, inject: [FeatureFlags, EnvConfig] },    CheckoutService, RefundService,  ],})export class AppModule {}
Pick one

Go deeper

Pro tip
  1. Before you trust an override, ask the compiled testing module which object answers for the token: moduleRef.get(TOKEN, { strict: false }) returns it, and the spec can assert that it is the fake. When: a spec overrides a client bound under a symbol or string token, or registered by a dynamic module (step 8). Verify:
    tip 1 MR !4030 spec: moduleRef.get(VendorIdCheckClient, { strict: false }) is the spec's fake: true
Source

The pages are vendor documentation that changes with releases, so their claims are labelled valid as of 2026-10-13; every NestJS behaviour this lesson grades is executed on NestJS 12.1.2.

Checked against the tools and docs as of 2026-10-13.