Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

NestJS  Module 3 – Providers and dependency injection

Injection scopes: singleton, request and transient

When Nest builds providers under each scope, why one request-scoped dependency makes its consumers request-scoped, and request context without it.

  • Intermediate
  • 15 minutes
  • Examples run with Node.js 24.21.0, NestJS 12.1.2, TypeScript 6.0.3 and quickjs 0.32.0
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: Three scopes decide when Nest builds a provider · One request-scoped dependency makes its consumers request-scoped. With a pass: 2 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Choose between the default, request and transient scopes for a provider
  • Predict how request scope bubbles up a chain of dependencies, and count the instances it builds
  • Resolve scoped providers with resolve() instead of get(), and identify the consumer with INQUIRER
  • Store request context in AsyncLocalStorage instead of using request scope

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose between the default, request and transient scopes for a provider
  • Predict how request scope bubbles up a chain of dependencies, and count the instances it builds
  • Resolve scoped providers with resolve() instead of get(), and identify the consumer with INQUIRER
  • Store request context in AsyncLocalStorage instead of using request scope

Before you start

On this page

One provider marked request scope: Scope.REQUEST: Nest builds one instance of the provider for each request that needs it and drops it after the response. Every class that injects such a provider becomes request-scoped too., RequestContext, holds the kitchen of the current request. After three requests, Nest had built OrdersService and OrdersController three times each as well, although neither asked for any scope: the service needs RequestContext and the controller needs the service, so the context’s scope became theirs.

Scopes decide when Nest builds a provider and who shares it. This lesson compares the three scopes, reproduces the way request scope spreads up a chain of dependencies, and counts what it costs; then it runs transient scope: Scope.TRANSIENT: each class that injects the provider gets an instance of its own, built when that class is built, instead of sharing one instance with the rest of the app. providers and a way to carry request data that keeps every provider a singleton.

Three scopes decide when Nest builds a provider

A provider’s scope is set in its decorator, @Injectable({ scope: Scope.REQUEST }), or with a scope key in a custom provider. A controller takes one too, in @Controller({ path, scope }), and every route of the controller then shares it. There are three scopes:

Scope Nest builds Shared by Built at
Scope.DEFAULT one instance for the app every class that asks start-up
Scope.REQUEST one instance per request the classes serving that request each request
Scope.TRANSIENT one instance per consumer nobody: each consumer has its own when its consumer is built

Scope.DEFAULT, the singleton of the previous lessons, is what a provider gets when it asks for nothing. Request scope gives each request its own object, which may receive the request itself through the REQUEST token of @nestjs/core; Nest drops it once the response is sent. Transient scope gives each class that injects the provider a private instance, which suits helpers such as a logger that labels its lines with its owner.

A request-scoped or transient provider is also fetched differently. app.get() returns singletons only, and resolve() builds scoped instances on demand. The INQUIRER token tells a transient instance which class it was built for, and Node.js’s AsyncLocalStorage: A Node.js class that keeps a store for all the asynchronous work started inside its run() call, so code deep inside a request can read request data without receiving it as a parameter. can carry request data to singletons without any scope at all.

Checkpoint

Check: The three scopes

  1. Question 1 of 2 A logger labels every line with the class that owns it. Which scope gives OrdersService and InvoicesService a logger each, so that one label never overwrites the other?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Scope.TRANSIENT

    Transient scope is one instance per consumer, built when the consumer is built, which suits helpers that keep something about their owner.

  2. Question 2 of 2 When does Nest build a request-scoped provider?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Once for each request that needs it, and never at start-up

    The table of the section: one instance per request, shared by the classes serving that request, built for each request.

One request-scoped dependency makes its consumers request-scoped

Here is the app of the opening. Only RequestContext asks for request scope; it reads the kitchen from the request:

bubbling/src/app.ts TypeScript · bubbling/src/app.ts
import { Controller, Get, Inject, Injectable, Module, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import type { Request } from 'express';
import { count } from './counts.js';

/** Request-scoped: one per request, built from that request. */
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
  readonly kitchen: string;
  constructor(@Inject(REQUEST) req: Request) {
    count('RequestContext');
    // A real app takes the kitchen from the signed-in user; a header keeps the example short.
    this.kitchen = String(req.headers['x-kitchen'] ?? 'unknown');
  }
}

/** Declared with the default scope, but it needs RequestContext. */
@Injectable()
export class OrdersService {
  readonly n = count('OrdersService');
  constructor(private readonly ctx: RequestContext) {}
  list() {
    return `orders of ${this.ctx.kitchen}`;
  }
}

/** The default scope, and nothing request-scoped below it. */
@Injectable()
export class MenuService {
  readonly n = count('MenuService');
  today() {
    return 'poha, dal rice';
  }
}

@Controller('orders')
export class OrdersController {
  readonly n = count('OrdersController');
  constructor(
    private readonly orders: OrdersService,
    private readonly menu: MenuService,
  ) {}

  /** Answers with the instance numbers of the objects that served this request. */
  @Get()
  list() {
    return { orders: this.orders.list(), controller: this.n, service: this.orders.n, menuService: this.menu.n };
  }
}

@Module({ controllers: [OrdersController], providers: [RequestContext, OrdersService, MenuService] })
export class AppModule {}

OrdersService needs RequestContext, and a singleton cannot hold an object that exists once per request. So Nest makes OrdersService request-scoped as well, and then OrdersController, which needs OrdersService. The documentation calls this scope bubbling: The rule that a class which needs a request-scoped provider, directly or through other classes, becomes request-scoped itself. It spreads up a chain of dependencies, never down. the injection chain. Step through start-up and three requests:

Interactive · step by step One request-scoped provider at the bottom of a chain Step through start-up and three requests. Watch which classes Nest builds once, and which it builds again for every request.
One request-scoped provider at the bottom of a chain, step 6 of 6. Showing OrdersController (request 3: #3), GET /orders, OrdersService (request 3: #3), MenuService (built at start-up: #1), RequestContext (request 3: #3). In focus: RequestContext, OrdersService, OrdersController.needsneedsneedshandled byOrdersControllerrequest 3: #3GET /ordersthree requestsOrdersServicerequest 3: #3MenuServicebuilt at start-up: #1RequestContextrequest 3: #3x-kitchen pune-07

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The declared scopes. Only RequestContext asks for request scope. OrdersService and OrdersController are written with the default scope, and so is MenuService.
  2. Start-up. MenuService needs nothing request-scoped, so Nest builds it once, while the app starts. The run printed "after start-up: MenuService 1" and nothing else.
  3. The scope bubbles up. OrdersService needs RequestContext, which exists only per request, so Nest makes OrdersService request-scoped too, and then OrdersController, which needs OrdersService.
  4. Request 1. A request from pune-07 arrives. Nest builds RequestContext #1 from it, then OrdersService #1 and OrdersController #1, and hands the controller the MenuService built at start-up.
  5. Request 2. The next request, from pune-12, gets a chain of its own, number 2 of each class. MenuService is still #1.
  6. Request 3. After three requests the run counted RequestContext 3, OrdersService 3 and OrdersController 3 against MenuService 1. Each chain is released once its response has been sent.

What Nest built at start-up and for three requests JavaScript · bubbling/run.mjs
import { session } from '../session.mjs';

// One request-scoped provider at the bottom of a chain: what Nest builds at start-up and for each of three requests.
session(`
npx tsc -p tsconfig.json
node dist/main.js
`);

Output

$ npx tsc -p tsconfig.json
$ node dist/main.js
after start-up: MenuService 1
GET /orders (x-kitchen: pune-07) -> {"orders":"orders of pune-07","controller":1,"service":1,"menuService":1}
GET /orders (x-kitchen: pune-12) -> {"orders":"orders of pune-12","controller":2,"service":2,"menuService":1}
GET /orders (x-kitchen: pune-07) -> {"orders":"orders of pune-07","controller":3,"service":3,"menuService":1}
after three requests: MenuService 1, RequestContext 3, OrdersService 3, OrdersController 3

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 run confirms each step. After start-up only MenuService existed. Every request then received a fresh RequestContext, OrdersService and OrdersController, numbered 1, 2 and 3 in the answers, while MenuService stayed #1. Scope bubbles up, never down: MenuService is needed by a request-scoped controller and stays a singleton, because it needs nothing request-scoped itself.

Key idea

A class becomes request-scoped when anything it needs is, directly or further down, so one request-scoped provider at the bottom of a chain rebuilds the whole chain above it on every request.

That is the cost to watch. This small chain builds 3 objects per request; at 2,000 requests a second that is 6,000 objects a second for the garbage collector, plus the work of every constructor. The documentation’s advice is to prefer singletons and use request scope only where it is truly needed. Nest gives no warning when a scope spreads: the only signs are constructors that run on every request and instance numbers that keep rising, which is why the run counts them.

Checkpoint

Check: Request scope bubbling up

  1. Question 1 of 2 In the bubbling run, how many objects of the four classes existed after start-up, before any request?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 1 object

    The run printed "after start-up: MenuService 1". RequestContext is request-scoped, and OrdersService and OrdersController became request-scoped because they need it, so none of the three was built at start-up.

  2. Question 2 of 2 MenuService is injected into OrdersController, which became request-scoped. Why did MenuService stay instance #1 for all three requests?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Scope bubbles up from a dependency to its consumers, never down to what a class needs

    A request-scoped class may receive singletons. The answers showed menuService 1 in every response.

Transient providers, INQUIRER and resolve(), run and counted Pro: opens with a Pro, Premium or Ultimate pass

Transient scope is easy to misread as “a new instance at every injection”, and app.get() cannot fetch it at all. This part runs both rules on a logger, together with the INQUIRER token.

In this part:

  • One instance per consumer, not per parameter
  • INQUIRER: the class a transient instance was built for
  • get() refuses scoped providers; resolve() builds a new one on each call
  • Scoped providers in tests, without surprises

2 runnable examples (JavaScript, TypeScript) · about 4 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.

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).

Request context without request scope: AsyncLocalStorage, counted Pro: opens with a Pro, Premium or Ultimate pass

Most request-scoped providers exist to read one or two values of the request, such as its id, its user or its kitchen. AsyncLocalStorage carries those values to singletons instead; this part runs three overlapping requests through it and counts what each approach builds.

In this part:

  • A store per request, read by a singleton after every await
  • What each approach builds per request, with numbers
  • When request scope is still the right tool

2 runnable examples (JavaScript, TypeScript (app.ts), TypeScript (context.ts)) · about 4 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.

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).

Practise: count what Nest builds

Write the prediction the bubbling run checked: given an app’s classes and their declared scopes, count the instances after a number of requests. The sample tests run in your browser and start from the lesson’s own app:

Exercise · Medium · JavaScript

Predict the instances request scope builds

Predict what Nest builds. countInstances(graph, requests) receives the classes of an app and the number of requests it has served, and returns how many instances of each class exist by then.

graph maps each class name to { scope, deps }: the scope it is declared with, 'default' or 'request', and the names of the classes its constructor needs. Every request uses every class once. Apply the two rules of this lesson:

  • a class is request-scoped when it is declared 'request', or when any class it needs is request-scoped, directly or through others: the scope bubbles up, never down;
  • a request-scoped class is built once for each request and never at start-up; any other class is built once, at start-up.

Return an object with one entry for every class of graph, in any order: { OrdersService: 3, MenuService: 1, … }. The graphs have no cycles.

Starter code · count_instances.mjs

/**
 * The number of instances of each class after start-up and `requests` requests. graph: { [name]: { scope, deps } },
 * scope 'default' or 'request'. Returns { [name]: count }.
 */
export function countInstances(graph, requests) {
  const counts = {};
  for (const name of Object.keys(graph)) {
    // Is it request-scoped, declared or because of what it needs?
    counts[name] = 1;
  }
  return counts;
}
The sample tests · count_instances.test.mjs
import { assert, test } from 'mysmartcopilot:test';
import { countInstances } from './count_instances.mjs';

/** The lesson's app: RequestContext is request-scoped, the other three are declared with the default scope. */
const lessonApp = {
  OrdersController: { scope: 'default', deps: ['OrdersService', 'MenuService'] },
  OrdersService: { scope: 'default', deps: ['RequestContext'] },
  MenuService: { scope: 'default', deps: [] },
  RequestContext: { scope: 'request', deps: [] },
};

test('the lesson app after three requests, as the run counted', () => {
  assert.deepEqual(countInstances(lessonApp, 3), { OrdersController: 3, OrdersService: 3, MenuService: 1, RequestContext: 3 });
});

test('after start-up only, request-scoped classes have no instance', () => {
  assert.deepEqual(countInstances(lessonApp, 0), { OrdersController: 0, OrdersService: 0, MenuService: 1, RequestContext: 0 });
});

test('the scope bubbles up through several levels', () => {
  const chain = { A: { scope: 'default', deps: ['B'] }, B: { scope: 'default', deps: ['C'] }, C: { scope: 'default', deps: ['D'] }, D: { scope: 'request', deps: [] } };
  assert.deepEqual(countInstances(chain, 5), { A: 5, B: 5, C: 5, D: 5 });
});

test('the scope never bubbles down to what a request-scoped class needs', () => {
  const graph = { Ctx: { scope: 'request', deps: ['Clock'] }, Clock: { scope: 'default', deps: [] } };
  assert.deepEqual(countInstances(graph, 4), { Ctx: 4, Clock: 1 });
});

test('a class with one request-scoped branch is request-scoped; the other branch is not', () => {
  const graph = {
    Top: { scope: 'default', deps: ['Left', 'Right'] },
    Left: { scope: 'default', deps: ['Ctx'] },
    Right: { scope: 'default', deps: ['Menu'] },
    Ctx: { scope: 'request', deps: [] },
    Menu: { scope: 'default', deps: [] },
  };
  assert.deepEqual(countInstances(graph, 2), { Top: 2, Left: 2, Right: 1, Ctx: 2, Menu: 1 });
});

test('a graph without request scope builds one of each, however many requests', () => {
  const graph = { Wallet: { scope: 'default', deps: ['Repo'] }, Repo: { scope: 'default', deps: [] } };
  assert.deepEqual(countInstances(graph, 1000), { Wallet: 1, Repo: 1 });
});

test('a thousand requests build a thousand of each request-scoped class', () => {
  const counts = countInstances(lessonApp, 1000);
  assert.equal(counts.OrdersService, 1000);
  assert.equal(counts.MenuService, 1);
});
A hint

Write a function isRequestScoped(name) first: it is true when the class is declared 'request', or when any of its deps is request-scoped, which is the same function called on the dependency. The count of a class is then requests when that function says true, and 1 otherwise.

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 three scopes and on bubbling:

Practice that adapts to you

Practise this lesson

7 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 When does Nest build a provider with the default scope?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: Once, while the app starts, for every class that asks for it

    Scope.DEFAULT is what a provider gets when it asks for nothing: one instance at start-up, shared by everyone.

  2. Question 2 of 7 Which sentence describes Scope.TRANSIENT?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: Each class that injects the provider gets an instance of its own

    Default: one for the app. Request: one per request. Transient: one per consumer.

  3. Question 3 of 7 A class collects the changes of one request and commits them at its end, and three services use it during that request. Which scope fits?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: Request scope, so the three services share one instance for that request only

    Request scope is the right tool when an object belongs to one request and several classes share it during that request.

  4. Question 4 of 7 A controller is declared with @Controller({ path: "orders", scope: Scope.REQUEST }). What does that mean for its routes?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: Every route of the controller runs on a new controller instance built for that request

    @Controller() accepts a scope like @Injectable(); a request-scoped controller is built for every request it handles.

  5. Question 5 of 7 A needs B, B needs C, and only C is declared request-scoped. How many instances of A exist after 10 requests?

    Type a number.

    Show the answer: Question 5 of 7

    Answer: 10 instances

    C is request-scoped, so B, which needs it, becomes request-scoped, and A, which needs B, does too. Each request builds one of each: 10 instances of A.

  6. Question 6 of 7 How did the bubbling run show that request scope had spread to OrdersService?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: Its instance number rose with every request, 1, 2, 3

    The answers carried "service":1, then 2, then 3, and the totals line counted OrdersService 3 after three requests.

  7. Question 7 of 7 You want OrdersService to stay a singleton, but it needs the kitchen of the current request. Which change does that?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: Stop injecting RequestContext into it, and pass the kitchen to its methods as an argument

    Scope is decided by what a class needs. Remove the need, by an argument or another way of reading request data, and the class stays a singleton.

Interview questions

  1. Fresher to mid · Warm-up

    What does request scope do in NestJS, and what happens to a singleton that injects a request-scoped provider?

    A provider declared with @Injectable({ scope: Scope.REQUEST }) gets one instance per request: Nest builds it when a request needs it, can pass it the request through the REQUEST token, and drops it after the response. It is never built at start-up. A class that injects it cannot stay a singleton, because one shared object cannot hold a different dependency for every request, so Nest makes that class request-scoped too, and every class above it. The scope bubbles up the chain, never down. In this lesson's run, one request-scoped RequestContext made OrdersService and OrdersController request-scoped: after three requests each existed three times, while MenuService, which needs nothing request-scoped, stayed a single instance.

  2. Mid to senior

    A teammate made a TenantContext provider request-scoped so that services can read the tenant of each request. Afterwards latency rose, garbage collection grew, and an onModuleInit() hook in a repository stopped running. Nothing else changed. Explain all three effects, and how you would keep per-request tenant data without them.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Your logger is a transient provider so that each class can label its own lines. A heap snapshot shows thousands of logger instances in an app with about 40 classes that inject it. What can cause that, and how would you find and fix the cause?

    Model answer, Pro. First step and follow-ups

  4. Mid to staff

    Every log line of a request, in every service, must carry that request's id, including lines written after awaits and in nested calls, and no service should become request-scoped. How would you build this in Nest, and how would you prove it works with many requests in flight?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The default scope builds one instance at start-up for everyone, request scope one per request, and transient scope one per consumer.
  • A class that needs a request-scoped provider becomes request-scoped itself, and so does everything above it; scope bubbles up, never down.
  • A request-scoped chain rebuilds every object in it on each request, so a deep chain under heavy traffic creates many objects for nothing more than one value of the request.
  • Scoped providers are fetched with resolve(), not get(), and INQUIRER tells a transient instance which class it was built for.

Terms in this lesson

AsyncLocalStorage
A Node.js class that keeps a store for all the asynchronous work started inside its run() call, so code deep inside a request can read request data without receiving it as a parameter.
request scope
Scope.REQUEST: Nest builds one instance of the provider for each request that needs it and drops it after the response. Every class that injects such a provider becomes request-scoped too.
scope bubbling
The rule that a class which needs a request-scoped provider, directly or through other classes, becomes request-scoped itself. It spreads up a chain of dependencies, never down.
transient scope
Scope.TRANSIENT: each class that injects the provider gets an instance of its own, built when that class is built, instead of sharing one instance with the rest of the app.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.