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?
// 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');}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
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?
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 {}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?
// 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 {}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?
// 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 {}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?
// 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');}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?
// 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');}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?
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 {}Go deeper
Pro tip
- 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.
- NestJS documentation, Custom providers (https://docs.nestjs.com/fundamentals/custom-providers), the curriculum row's source "Providers and Custom providers": read from https://raw.githubusercontent.com/nestjs/docs.nestjs.com/master/content/fundamentals/dependency-injection.md (
nestjs-custom-providers.txt). Teaching for steps 2–6. - NestJS documentation, Testing (https://docs.nestjs.com/fundamentals/testing), the row's second source: read from https://raw.githubusercontent.com/nestjs/docs.nestjs.com/master/content/fundamentals/unit-testing.md (
nestjs-testing.txt). Teaching for steps 7 and 15. - NestJS documentation, Circular dependency (https://docs.nestjs.com/fundamentals/circular-dependency): read from https://raw.githubusercontent.com/nestjs/docs.nestjs.com/master/content/fundamentals/circular-dependency.md (
nestjs-circular-dependency.txt). Teaching for steps 10–12. - NestJS documentation, Dynamic modules (https://docs.nestjs.com/fundamentals/dynamic-modules): read from https://raw.githubusercontent.com/nestjs/docs.nestjs.com/master/content/fundamentals/dynamic-modules.md (
nestjs-dynamic-modules.txt). Teaching for steps 13–14.
Related lessons
- Read first: Dependency inversion and NestJS dependency injection
- Related: Modular monolith vs microservices
- Related: Test doubles and boundaries
- Related: Provider scopes and request context: singleton fields and AsyncLocalStorage
- Related: Testing NestJS: modules, overrides and a real database
- Related: Factory, abstract factory and NestJS useFactory
Checked against the tools and docs as of 2026-10-13.