NestJS Module 3 – Providers and dependency injection
Custom providers, tokens and factories
How useValue, useClass, useFactory and useExisting give Nest providers that are not plain classes, and how symbol and string tokens are injected.
What you will learn
- Configure values, classes, factories and aliases as providers with useValue, useClass, useFactory and useExisting
- Use symbol and string tokens with @Inject(), and choose symbols so that tokens cannot collide
- Write async factory providers with optional dependencies and a deadline on start-up
- Check which instance an alias or a test override resolves to, on the Nest version you run
Before you start
What you’ll learn · Before you start
What you will learn
- Configure values, classes, factories and aliases as providers with useValue, useClass, useFactory and useExisting
- Use symbol and string tokens with @Inject(), and choose symbols so that tokens cannot collide
- Write async factory providers with optional dependencies and a deadline on start-up
- Check which instance an alias or a test override resolves to, on the Nest version you run
Before you start
On this page
TiffinTrail’s lunch reminder needs four things that are not plain classes: its settings, which are a plain object; an SMS gateway that is fake in tests and live in production; a reminder built from those two; and a second name for the gateway that older code asks for. Constructor types cannot express any of the four.
custom provider: A providers entry written out as { provide: token, … } with one recipe, useValue, useClass, useFactory or useExisting, instead of a bare class. It lets a token stand for a value, a chosen class or what a function returns. can. Each entry in providers pairs a token with a recipe that says what Nest should inject for it,
and a token that is not a class is injected by name with @Inject(). This lesson runs all four recipes in one module,
then shows how factory provider: A custom provider with useFactory: its value is what a function returns. Nest calls the function once, passing the providers listed in inject in that order. hold start-up until a connection is ready, and where tokens and test overrides surprise
people.
A provider entry pairs a token with a recipe
The bare class in providers: [WalletService] is short for { provide: WalletService, useClass: WalletService }.
Written out, every entry has a token (provide) and one of four recipes:
useValueinjects one existing object exactly as written, such as settings or a test double.useClassmakes Nest build a class for the token, a class you can choose per environment.useFactoryinjects whatever a function returns; Nest calls it once with the providers listed ininject.useExistinggives a second token to a provider that already exists, so both names reach one instance.
| Recipe | What Nest injects | What Nest builds | Typical use |
|---|---|---|---|
useValue |
the value you wrote | nothing | settings, constants, a fake in a test |
useClass |
an instance of the class | the class, once | an implementation picked per environment |
useFactory |
what the function returns | calls the function, once | objects built from other providers, connections |
useExisting |
the other token’s instance | nothing new | an old name kept for a provider |
Here is one module with all four. Step through how Nest reads its entries, then run it:
import { Module } from '@nestjs/common';
import { LunchJob } from './lunch-job.js';
import {
CUT_OFF_REMINDER,
CutOffReminder,
FakeSmsGateway,
LiveSmsGateway,
NOTIFIER,
SMS_CONFIG,
SMS_GATEWAY,
type SmsConfig,
type SmsGateway,
} from './sms.js';
@Module({
providers: [
// useValue: this exact object.
{ provide: SMS_CONFIG, useValue: { senderId: 'TFNTRL', cutOff: '10:30' } },
// useClass: Nest builds this class; the condition picks it once, when this file loads.
{ provide: SMS_GATEWAY, useClass: process.env.SMS_MODE === 'live' ? LiveSmsGateway : FakeSmsGateway },
// useFactory: whatever the function returns, called with the providers of inject, in that order.
{
provide: CUT_OFF_REMINDER,
useFactory: (config: SmsConfig, gateway: SmsGateway) => new CutOffReminder(config, gateway),
inject: [SMS_CONFIG, SMS_GATEWAY],
},
// useExisting: a second token for the provider that SMS_GATEWAY names.
{ provide: NOTIFIER, useExisting: SMS_GATEWAY },
LunchJob,
],
})
export class AppModule {} The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- useValue. The first entry gives the token SMS_CONFIG an object, written in the module. Nest builds nothing: every class that asks for SMS_CONFIG receives this exact object.
- useClass. SMS_GATEWAY names a class. With SMS_MODE unset, the condition in the module picked FakeSmsGateway, and Nest builds it once, as it builds any provider class.
- useFactory. CUT_OFF_REMINDER is whatever the factory returns. Nest calls it once, passing the providers listed in inject, in that order, SMS_CONFIG first and SMS_GATEWAY second.
- useExisting. NOTIFIER is an alias. Nest does not build a second gateway; asking for NOTIFIER returns the instance that SMS_GATEWAY already names, and the run printed true for that.
- A consumer. LunchJob asks for the reminder with @Inject(CUT_OFF_REMINDER). A symbol is not a type the compiler can record, so the decorator names the token.
import { session } from '../session.mjs';
// The four recipes of one module, run twice: with the default gateway, then with SMS_MODE=live.
session(`
npx tsc -p tsconfig.json
node dist/main.js
SMS_MODE=live node dist/main.js
`); Output
$ npx tsc -p tsconfig.json
$ node dist/main.js
SMS_MODE=(not set)
SMS_CONFIG {"senderId":"TFNTRL","cutOff":"10:30"}
SMS_GATEWAY an instance of FakeSmsGateway
LunchJob.run() kept in memory (1 so far); kept in memory (2 so far)
NOTIFIER the same object as SMS_GATEWAY: true
$ SMS_MODE=live node dist/main.js
SMS_MODE=live
SMS_CONFIG {"senderId":"TFNTRL","cutOff":"10:30"}
SMS_GATEWAY an instance of LiveSmsGateway
LunchJob.run() would send 36 characters to cus-41; would send 36 characters to cus-42
NOTIFIER the same object as SMS_GATEWAY: true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs
The two runs differ only where the gateway does: the condition in useClass chose FakeSmsGateway when SMS_MODE
was unset and LiveSmsGateway with SMS_MODE=live, so the reminders of LunchJob went through another gateway while
the module and every consumer stayed the same. In both runs NOTIFIER was the same object as SMS_GATEWAY, because an alias provider: A custom provider with useExisting: a second token for a provider that already exists, so both tokens reach the same instance. reuses an instance instead
of building one.
Checkpoint
Check: The four provider recipes
Symbols, strings and classes as tokens
A class token needs no help: the compiler records the parameter’s type and Nest looks that class up. Custom providers
often use tokens that are not classes, and then the recorded type cannot help: an interface such as SmsGateway is
recorded as Object. Such a parameter names its token with @Inject():
import { Inject, Injectable } from '@nestjs/common';
import { CUT_OFF_REMINDER, type CutOffReminder } from './sms.js';
/** A consumer: a token that is not a class needs @Inject(token) on the parameter. */
@Injectable()
export class LunchJob {
constructor(@Inject(CUT_OFF_REMINDER) private readonly reminder: CutOffReminder) {}
run(customers: string[]) {
return customers.map((c) => this.reminder.remind(c));
}
} Strings and symbols both work as tokens, and they differ in one way that matters. A symbol token: An injection token made with Symbol(). Every Symbol() call creates a new value, so two modules cannot register the same symbol token by accident, unlike two strings with the same text. made with Symbol() is
unique: two modules that each create Symbol('CONFIG') hold two different tokens. Two string tokens with the same
text are one token, so when two imported modules both export a 'CONFIG', the importer gets one of them, without
an error. Define each token once, as an exported symbol in a file of its own, and import it wherever it is provided or
injected.
| Token | How a consumer asks for it | Can two modules clash? |
|---|---|---|
a class, such as WalletService |
by its type alone | no: two classes are two tokens |
an abstract class, such as WalletRepository |
by its type alone | no |
a string, such as 'CONFIG' |
@Inject('CONFIG') |
yes: the same text is the same token |
a symbol, such as SMS_GATEWAY |
@Inject(SMS_GATEWAY) |
no: every Symbol() call is a new token |
An interface as the type, and no @Inject()
constructor(private readonly gateway: SmsGateway) compiles, and Nest refuses to start, because the compiler
recorded Object for the interface. Add @Inject(SMS_GATEWAY) to the parameter: the type stays for the compiler,
and the decorator gives Nest the token.
Checkpoint
Check: Tokens that are not classes
Factories that wait, fall back and fail fast Pro: opens with a Pro, Premium or Ultimate pass
A factory may return a promise, and Nest waits for it before it builds anything that needs the result. The program below runs three start-ups: a connection that answers, one that never does, and the same one with a deadline.
In this part:
- An async factory holds start-up until it resolves, step by step
- Optional dependencies of a factory resolve to undefined
- A connection that never answers: the hang reproduced, and a deadline that fixes it
- Where to open a connection: factory, hook or constructor, compared
2 runnable examples (JavaScript, TypeScript) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Token collisions, aliases and test overrides, with two version traps reproduced Pro: opens with a Pro, Premium or Ultimate pass
Three things look interchangeable and are not: two string tokens with the same text, an alias and a second class provider, and an override in a test on two Nest versions. Each one is run here, and the output decides.
In this part:
- Two modules, one string token: the silent collision reproduced
- useExisting shares an instance; a second useClass builds another
- Two version traps between 12.0.4 and 12.1.2, run on both
4 runnable examples (JavaScript, TypeScript) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: resolve the four recipes
Write the small container that the first section described: values, classes, factories and aliases, each built at most once. Its sample tests run in your browser and use the lesson’s own tokens:
Exercise · Medium · JavaScript
Resolve the four provider recipes
Teach a small container Nest's four recipes. createContainer(entries) receives the entries of a module's providers, each with a token (provide) and one recipe:
{ provide, useValue }: the token is that exact value;{ provide, useClass, deps }: an instance of the class, built withnew useClass(...)and the instances of the tokens indeps, in that order;{ provide, useFactory, inject }: whateveruseFactory(...)returns, called with the instances of the tokens ininject, in that order;{ provide, useExisting }: the same instance as the token it names.
Return { get }. get(token) returns the token's instance and builds it on first use; every later get and every entry that needs the token receive that same instance, so each class is built once and each factory is called once. Entries may need tokens listed after them. For a token that no entry provides, throw an Error with the message no provider for <token>, the token converted with String(): a symbol made by Symbol('SMS_GATEWAY') is written Symbol(SMS_GATEWAY).
Starter code · recipes.mjs
/**
* A container for Nest's four recipes: entries { provide, useValue }, { provide, useClass, deps },
* { provide, useFactory, inject } and { provide, useExisting }. Returns { get(token) }.
*/
export function createContainer(entries) {
const byToken = new Map(entries.map((e) => [e.provide, e]));
return {
get(token) {
const entry = byToken.get(token);
// Handle each recipe, build each token once, and fail for a token without an entry.
return entry?.useValue;
},
};
} The sample tests · recipes.test.mjs
import { assert, test } from 'mysmartcopilot:test';
import { createContainer } from './recipes.mjs';
const SMS_CONFIG = Symbol('SMS_CONFIG');
const SMS_GATEWAY = Symbol('SMS_GATEWAY');
const CUT_OFF_REMINDER = Symbol('CUT_OFF_REMINDER');
const NOTIFIER = Symbol('NOTIFIER');
class FakeSmsGateway {
static made = 0;
constructor() {
FakeSmsGateway.made += 1;
}
}
/** The lesson's module: the four recipes, the alias listed before the token it names. */
function lessonModule() {
FakeSmsGateway.made = 0;
const calls = [];
const config = { senderId: 'TFNTRL', cutOff: '10:30' };
const container = createContainer([
{ provide: NOTIFIER, useExisting: SMS_GATEWAY },
{ provide: SMS_CONFIG, useValue: config },
{ provide: SMS_GATEWAY, useClass: FakeSmsGateway, deps: [] },
{ provide: CUT_OFF_REMINDER, useFactory: (c, g) => (calls.push([c, g]), { config: c, gateway: g }), inject: [SMS_CONFIG, SMS_GATEWAY] },
]);
return { container, calls, config };
}
test('useValue gives the exact object, not a copy', () => {
const { container, config } = lessonModule();
assert.equal(container.get(SMS_CONFIG), config);
});
test('useClass builds the class once, however often it is asked for', () => {
const { container } = lessonModule();
assert.ok(container.get(SMS_GATEWAY) instanceof FakeSmsGateway);
assert.equal(container.get(SMS_GATEWAY), container.get(SMS_GATEWAY));
assert.equal(FakeSmsGateway.made, 1);
});
test('useFactory receives the instances of inject, in order, and is called once', () => {
const { container, calls, config } = lessonModule();
const reminder = container.get(CUT_OFF_REMINDER);
container.get(CUT_OFF_REMINDER);
assert.equal(calls.length, 1);
assert.equal(reminder.config, config);
assert.equal(reminder.gateway, container.get(SMS_GATEWAY));
});
test('useExisting returns the same instance as the token it names, even when listed first', () => {
const { container } = lessonModule();
assert.equal(container.get(NOTIFIER), container.get(SMS_GATEWAY));
assert.equal(FakeSmsGateway.made, 1);
});
test('useClass passes the instances of deps to the constructor', () => {
class Reminder {
constructor(config, gateway) {
this.config = config;
this.gateway = gateway;
}
}
const container = createContainer([
{ provide: 'Reminder', useClass: Reminder, deps: ['Config', 'Gateway'] },
{ provide: 'Config', useValue: { cutOff: '10:30' } },
{ provide: 'Gateway', useClass: FakeSmsGateway, deps: [] },
]);
const r = container.get('Reminder');
assert.deepEqual(r.config, { cutOff: '10:30' });
assert.equal(r.gateway, container.get('Gateway'));
});
test('a value of 0 or false is still a provided value', () => {
const container = createContainer([{ provide: 'RETRIES', useValue: 0 }, { provide: 'LIVE', useValue: false }]);
assert.equal(container.get('RETRIES'), 0);
assert.equal(container.get('LIVE'), false);
});
test('a token without an entry throws, a symbol written as String() writes it', () => {
const container = createContainer([{ provide: NOTIFIER, useExisting: SMS_GATEWAY }]);
assert.throws(() => container.get('Kitchen'), 'no provider for Kitchen', 'get should throw this exact message');
assert.throws(() => container.get(NOTIFIER), 'no provider for Symbol(SMS_GATEWAY)', 'an alias to a missing token fails with the missing token');
}); A hint
Index the entries by token in a Map, and keep a second Map for the instances already made. Write one function, make(token), that returns the stored instance when there is one, and otherwise looks at which of the four keys the entry has: return the value, call make on each of deps or inject before calling the class or the factory, or return make(useExisting) for an alias. Store the result before returning it.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then the questions on the recipes and the tokens:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher to mid · Warm-up
Name the four recipes of a custom provider in NestJS, and say what each one injects.
A custom provider is an entry { provide: token, ... } with one recipe. useValue injects an object exactly as written, such as settings or a test double; Nest builds nothing. useClass makes Nest build a class for the token, which lets a module pick an implementation per environment while consumers keep asking for the same token. useFactory injects whatever a function returns: Nest calls it once and passes the providers listed in inject, in that order. useExisting gives a second token to a provider that already exists, so both tokens reach one instance. A token that is not a class, such as a symbol, is injected with @Inject(token) on the parameter. In this lesson's module the four recipes gave SMS_CONFIG, SMS_GATEWAY, CUT_OFF_REMINDER and NOTIFIER, and NOTIFIER was the same object as SMS_GATEWAY.
-
Mid to senior
Your service sends SMS through a gateway that must be fake in tests, a provider's sandbox in development and the live API in production, with a metrics client that only production has. How would you wire that in Nest so that no consumer knows which one it got?
-
Mid to senior
After a network change, a Nest service never finishes starting: no error, no log line after the database client begins to connect, and the platform restarts it every few minutes. The client is created by an async factory provider. Explain what is happening and how you would make the failure visible.
-
Mid to senior
An integration test replaces a configuration provider with overrideProvider() and a factory, and it passes, yet a colleague finds that it reads the production rate table. How would you work out why, and how would you keep tests that rely on overrides honest from now on?
Key takeaways
- A provider entry pairs a token with one recipe:
useValuefor an existing object,useClassfor a class Nest builds,useFactoryfor what a function returns, anduseExistingfor a second name of an existing provider. - A factory receives the providers listed in
inject, in that order, anduseClasscan pick an implementation per environment without changing any consumer. - A token that is not a class, such as a symbol for an interface, is injected with
@Inject(TOKEN). - Prefer exported symbols to strings: two string tokens with the same text are one token, and an importer gets one of them without an error.
Terms in this lesson
- alias provider
- A custom provider with useExisting: a second token for a provider that already exists, so both tokens reach the same instance.
- custom provider
- A providers entry written out as { provide: token, … } with one recipe, useValue, useClass, useFactory or useExisting, instead of a bare class. It lets a token stand for a value, a chosen class or what a function returns.
- factory provider
- A custom provider with useFactory: its value is what a function returns. Nest calls the function once, passing the providers listed in inject in that order.
- symbol token
- An injection token made with Symbol(). Every Symbol() call creates a new value, so two modules cannot register the same symbol token by accident, unlike two strings with the same text.
References
- NestJS documentation: Custom providers (NestJS)
- NestJS documentation: Asynchronous providers (NestJS)
- NestJS documentation: Testing (overriding providers) (NestJS)
- NestJS documentation: Providers (optional providers) (NestJS)
- NestJS documentation: Migration guide (optional markers in subclasses) (NestJS)
- NestJS v12.1.0 release notes (NestJS (GitHub))
- NestJS v12.1.1 release notes (NestJS (GitHub))
Report a problem with this lesson
Kept only in this browser. Your Learn progress