NestJS Enterprise Backend APIs · บทเรียน

ตัวให้บริการตามขอบเขตคำขอและข้อแลกเปลี่ยน

ใช้ขอบเขต REQUEST อย่างปลอดภัย พร้อมทำความเข้าใจผลกระทบด้านประสิทธิภาพและการแพร่ขอบเขตของการฉีด

บทเรียน 4 จาก 413 ขั้นตอน

ตัวให้บริการตามขอบเขตคำขอและข้อแลกเปลี่ยน เป็นบทเรียน NestJS Enterprise Backend APIs ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน NestJS Enterprise Backend APIs และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน

บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ

Why Scopes Exist

By default every NestJS provider is a singleton: one instance is created at bootstrap and shared across the whole application lifetime. This is fast and memory-efficient, and it is the right choice for the overwhelming majority of services.

But some scenarios need per-request state. In a multi-tenant backend you might want a provider that already knows which tenant the current request belongs to, so you do not pass the tenant id through every method call. NestJS solves this with injection scopes.

  • Scope.DEFAULT — singleton (the default)
  • Scope.REQUEST — a new instance per incoming request
  • Scope.TRANSIENT — a new instance for each consumer that injects it

Declaring a Request-Scoped Provider

You opt into request scope by passing { scope: Scope.REQUEST } to the @Injectable() decorator. NestJS will then instantiate a fresh copy of this provider for every HTTP request (or message, in microservices/WebSocket transports).

Because a new instance exists per request, it is safe to store request-specific mutable state on it — concurrent requests never share the same object.

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
  private tenantId: string | null = null;

  set(id: string): void {
    this.tenantId = id;
  }

  get(): string {
    if (!this.tenantId) {
      throw new Error('Tenant not resolved for this request');
    }
    return this.tenantId;
  }
}

Injecting the REQUEST Object

A request-scoped provider can inject the underlying request object using the REQUEST token. This is the canonical way to read headers, the resolved user, or a subdomain to determine the tenant.

Only providers that are themselves request-scoped (or transient) may inject REQUEST. Trying to inject it into a singleton is a design error — the request does not exist yet when a singleton is built.

import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';

@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
  readonly tenantId: string;

  constructor(@Inject(REQUEST) private readonly req: Request) {
    const header = this.req.headers['x-tenant-id'];
    this.tenantId = Array.isArray(header) ? header[0] : (header ?? 'public');
  }
}

Scope Bubbling: The Core Trade-off

This is the single most important consequence to internalize: scope bubbles up the injection chain.

If a controller or service injects a request-scoped provider, that consumer also becomes request-scoped — even if you never marked it that way. The effect cascades transitively up every provider that depends on it.

  • A request-scoped TenantContext injected into OrdersService makes OrdersService request-scoped.
  • If OrdersController injects OrdersService, the controller is instantiated per request too.

One small request-scoped leaf can quietly turn a large subtree of your app non-singleton, which has real performance implications.

What Bubbling Looks Like in Code

Here neither OrdersService nor the controller declares a scope, yet both become request-scoped purely because of the transitive dependency on TenantContext. NestJS resolves the effective scope by taking the narrowest scope in the chain.

Keep this graph shallow: the deeper a request-scoped provider sits, the more of your tree it drags into per-request instantiation.

import { Injectable } from '@nestjs/common';
import { TenantContext } from './tenant.context';

// No scope declared, but it INHERITS Scope.REQUEST
@Injectable()
export class OrdersService {
  constructor(private readonly tenant: TenantContext) {}

  findAll() {
    return `orders for tenant ${this.tenant.get()}`;
  }
}

The Performance Cost

Request scope is not free. For every request, Nest must:

  • Walk the dependency subgraph and build a fresh instance of each request-scoped (and inheriting) provider.
  • Run their constructors and any lifecycle hooks on every request.
  • Garbage-collect those instances after the request ends.

Under high throughput this adds measurable latency and GC pressure. The official guidance is blunt: use request scope sparingly. The more providers turn request-scoped via bubbling, the larger the per-request allocation cost grows.

Lifecycle Hooks Run Per Request

A subtle gotcha: for request-scoped providers, lifecycle hooks like onModuleInit are not available, but request-scoped instances do go through construction on each request. Heavy setup in a constructor therefore runs on every single request.

If your provider opens a connection, parses a token, or hydrates config in its constructor, multiply that work by your request rate. Move expensive, request-independent setup into a singleton and inject it.

import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { ConnectionPool } from './connection-pool'; // singleton

@Injectable({ scope: Scope.REQUEST })
export class TenantConnection {
  // pool is shared (singleton); only the cheap per-request pick is here
  constructor(
    @Inject(REQUEST) req: Request,
    private readonly pool: ConnectionPool,
  ) {
    const tenant = (req.headers['x-tenant-id'] as string) ?? 'public';
    this.client = pool.forTenant(tenant);
  }
  client: unknown;
}

Performance-Sensitive REQUEST Injection

For performance-critical request-scoped providers, Nest lets you avoid inheriting the full Request object payload. Passing { scope: Scope.REQUEST } still injects a lightweight context.

In some transports (e.g. GraphQL, microservices) the REQUEST token actually resolves to the execution context, not an HTTP request. Write your provider to read only what it needs, and guard for the shape differences across transports rather than assuming Express.

import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';

interface MaybeHttp {
  headers?: Record<string, string | string[] | undefined>;
}

@Injectable({ scope: Scope.REQUEST })
export class RequestMeta {
  readonly correlationId: string;

  constructor(@Inject(REQUEST) ctx: MaybeHttp) {
    const raw = ctx.headers?.['x-correlation-id'];
    this.correlationId = (Array.isArray(raw) ? raw[0] : raw) ?? 'n/a';
  }
}

Durable Providers: Scaling Multi-Tenancy

For multi-tenant apps where you would otherwise make many providers request-scoped, Nest offers durable providers. The idea: instead of one instance per request, keep one instance per tenant and reuse it across that tenant's requests.

You define a ContextIdStrategy that maps a request to a stable sub-tree id (e.g. the tenant id). Nest then caches durable sub-trees keyed by that id, dramatically cutting instantiation cost while preserving per-tenant isolation.

import { Injectable, Scope } from '@nestjs/common';

// durable: true tells Nest these instances can be reused across
// requests that share the same context id (e.g. the same tenant).
@Injectable({ scope: Scope.REQUEST, durable: true })
export class TenantRepository {
  // resolved once per tenant context, not once per request
}

Resolving Scoped Providers Manually

Outside the normal injection flow — for example inside a singleton that occasionally needs a request-scoped provider — you cannot just inject it (that would bubble the singleton). Instead resolve it manually against the current context id using ModuleRef.

You must pass the request's ContextId so Nest returns the instance bound to that specific request, not a brand-new orphan instance.

import { Injectable } from '@nestjs/common';
import { ModuleRef, ContextIdFactory } from '@nestjs/core';
import { TenantContext } from './tenant.context';

@Injectable() // stays a SINGLETON
export class AuditService {
  constructor(private readonly moduleRef: ModuleRef) {}

  async logFor(req: unknown): Promise<string> {
    const contextId = ContextIdFactory.getByRequest(req as object);
    const ctx = await this.moduleRef.resolve(TenantContext, contextId);
    return `audited tenant ${ctx.get()}`;
  }
}

When NOT to Use Request Scope

Reach for request scope only when per-request state is genuinely needed and cannot be passed as a parameter. Common safer alternatives:

  • AsyncLocalStorage (Node's node:async_hooks) to carry request context without making providers request-scoped — the whole graph stays singleton.
  • Method parameters — just pass the tenant id explicitly.
  • Durable providers — when you need per-tenant isolation at scale.

This snippet shows the AsyncLocalStorage pattern, which keeps services as fast singletons while still exposing per-request data.

import { AsyncLocalStorage } from 'node:async_hooks';

type Store = { tenantId: string };
const als = new AsyncLocalStorage<Store>();

function handleRequest(tenantId: string, work: () => string): string {
  return als.run({ tenantId }, work);
}

function currentTenant(): string {
  return als.getStore()?.tenantId ?? 'public';
}

console.log(handleRequest('acme', () => `serving ${currentTenant()}`));
console.log(handleRequest('globex', () => `serving ${currentTenant()}`));

Quick Check

Consider what happens to the dependency graph when you introduce a request-scoped provider deep in your application.

Recap

Key takeaways for using request scope safely:

  • Default to singletons. Request scope exists for genuine per-request state like tenant context.
  • Scope bubbles up. One request-scoped leaf turns every dependent provider request-scoped, transitively.
  • It costs performance. Per-request construction, lifecycle work, and GC pressure scale with throughput.
  • Keep request-scoped providers shallow and cheap. Push heavy, request-independent setup into singletons.
  • Prefer alternatives when you can: AsyncLocalStorage for context, explicit parameters, and durable providers for per-tenant reuse at scale.
  • Resolve manually via ModuleRef.resolve with the request's ContextId when a singleton must reach a scoped instance.
เริ่มต้นได้ฟรี

เรียนรู้ TypeScript ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
20
บทเรียน
76

คำถามที่พบบ่อย

บทเรียน “ตัวให้บริการตามขอบเขตคำขอและข้อแลกเปลี่ยน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ตัวให้บริการตามขอบเขตคำขอและข้อแลกเปลี่ยน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส NestJS Enterprise Backend APIs ให้อัปเกรดเป็น CoddyKit PRO คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ตัวให้บริการตามขอบเขตคำขอและข้อแลกเปลี่ยน”

ใช้ขอบเขต REQUEST อย่างปลอดภัย พร้อมทำความเข้าใจผลกระทบด้านประสิทธิภาพและการแพร่ขอบเขตของการฉีด คุณปฏิบัติ NestJS Enterprise Backend APIs ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน NestJS Enterprise Backend APIs หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน NestJS Enterprise Backend APIs บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “ตัวให้บริการตามขอบเขตคำขอและข้อแลกเปลี่ยน” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน NestJS Enterprise Backend APIs นี้ได้ไหม

ได้ บทเรียน NestJS Enterprise Backend APIs ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การระบุผู้เช่าผ่านมิดเดิลแวร์และ AsyncLocalStorage
  2. การเชื่อมต่อฐานข้อมูลแบบแยกสคีมาตามผู้เช่า
  3. การสร้างโมดูลไดนามิกที่กำหนดค่าได้
  4. ตัวให้บริการตามขอบเขตคำขอและข้อแลกเปลี่ยน
← กลับไปที่ NestJS Enterprise Backend APIs