PAS7 Studio
До всіх статей

Проблема контексту запиту в NestJS: REQUEST-scoped DI vs AsyncLocalStorage (ALS) — практичний продакшн-посібник (2026)

Глибокий, підтверджений джерелами гайд про найболючішу проблему NestJS: request context (user, tenant, correlation IDs, tracing) без перетворення DI-графу на REQUEST scope. Порівняння рішень, реальні патерни та продакшн-посібник з @pas7/nestjs-request-context.

08 лют. 2026 р.· 11 хв читання· Технології
NestJS request context: request-scoped DI vs AsyncLocalStorage (ALS), logging and tracing

Це не «пост про теорію». Це практичний гайд, який можна застосувати в реальному NestJS-коді — з перевіреними джерелами й чіткою логікою вибору.

  • Що таке «request context» у NestJS і чому це болить. [1][2][3]

  • Чому REQUEST-scoped провайдери здаються простими, але часто стають пасткою для продуктивності й архітектури. [1]

  • Як AsyncLocalStorage (ALS) закриває основний біль (і де воно все ще може ламатись). [2][3]

  • Як зазвичай роблять компанії: correlation IDs, tracing, OpenTelemetry propagation, безпечні межі контексту. [4][5][6][7]

  • Порівняння найрелевантніших пакетів (плюси/мінуси, для яких задач підходить кожен). [8][9][10][11][12]

  • Продакшн-посібник + патерни коду (HTTP + черги + мікросервіси). [2][6][7]

  • Де саме підходить @pas7/nestjs-request-context і коли це конкурентний вибір. [10]

«Request context» — це все, що ви хочете читати глибоко в сервісах без прокидування через кожен метод: поточний user, tenant, permissions, request ID, trace IDs, handle транзакції, locale, feature flags тощо.

У NestJS ви зазвичай приходите до одного з цих варіантів:

- Ви прокидуєте контекст параметрами всюди (надійно, але шумно й складно підтримувати).

- Ви використовуєте REQUEST-scoped провайдери (простий API, але це може зробити значну частину застосунку такою, що створюється на кожен запит). [1]

- Ви використовуєте ALS-базований контекст (чисті точки виклику, singleton-провайдери, але потрібен правильний сетап і розуміння крайових сценаріїв). [2][3]

NestJS прямо попереджає: request-scoped провайдери додають накладні витрати, бо інстанси створюються на кожен запит, і — що гірше — scope «поширюється» по залежностях. [1]

Дві цитати, які реально важливі для архітектури:

> “Any provider that relies on a request-scoped provider automatically adopts a request scope, and this behavior cannot be altered.” [1]

> “Using request-scoped providers will have an impact on application performance... it will still have to create an instance of your class on each request.” [1]

Саме так реальні застосунки заходять у глухий кут: один «request-scoped datasource / logger / context» стає кореневою залежністю — і контролери, сервіси та репозиторії починають відтворюватись на кожен запит. Nest окремо згадує multi-tenant як типовий сценарій. [1]

Так, Nest пише, що impact може бути «~5% latency-wise» у добре спроєктованому додатку — але справжня ціна зазвичай архітектурна: важче мислити про lifetime, кеші й форму DI-дерева. [1]

Node.js AsyncLocalStorage — це по суті «thread-local storage для async коду»: ви асоціюєте стан з ланцюжком виконання (promises/callbacks) і читаєте його пізніше без параметрів. [3]

Node рекомендує AsyncLocalStorage як кращий шлях, ніж власна реалізація на async_hooks, описуючи ALS як продуктивне й memory-safe рішення. [3]

Але це не магія. Втрата контексту можлива в «рідкісних ситуаціях», особливо з callback-based API або кастомними thenables — Node рекомендує promisify або AsyncResource для коректного биндингу контексту. [3]

Питання стає таким: чи можемо ми надійно обгорнути Nest request lifecycle так, щоб все downstream бачило один і той самий store? Відповідь NestJS: так — через middleware / ранню точку входу. [2]

NestJS прямо позиціонує ALS як спосіб прокидати стан без параметрів і як альтернативу REQUEST-scoped провайдерам «та деяким їх обмеженням». [2]

Ключова ідея: обгорнути запит якнайраніше (middleware) через als.run(store, () => next()). Тоді будь-який провайдер може читати зі store пізніше.

Мінімальний NestJS-приклад (спрощено з офіційного рецепту): [2]

TS
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage<{ requestId: string; userId?: string }>();

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply((req: any, _res: any, next: () => void) => {
        const store = {
          requestId: String(req.headers['x-request-id'] ?? crypto.randomUUID()),
          userId: req.headers['x-user-id'] ? String(req.headers['x-user-id']) : undefined,
        };
        als.run(store, () => next());
      })
      .forRoutes('*path');
  }
}

Це «чисте ядро». У продакшні ви додаєте: безпечний парсинг хедерів, echo x-request-id у відповідь, trace correlation, queue propagation та тести.

У продакшні request context — це не лише «поточний user». Найчастіший драйвер — observability та інцидент-реакція: потрібно корелювати логи, трейси й метрики між сервісами.

Саме це робить OpenTelemetry context propagation: переносить trace/span контекст через межі процесів і мережі (типово через W3C Trace Context, зокрема traceparent). [6][7]

Два практичні висновки з enterprise-реальності:

- In-process контекст можна тримати в ALS, але між сервісами його треба переносити явно (HTTP headers, metadata повідомлень тощо). [6][7]

- Не кладіть чутливі дані в контекст, який ви пропагаєте між сервісами. OpenTelemetry прямо попереджає не передавати secrets/PII у baggage-подібних ключах. [6]

Операційна нотатка 2026, яку не можна ігнорувати, якщо ви сильно зав’язуєтесь на ALS (прямо чи через APM/tracing): у січні 2026 Node.js випустив mitigation для edge-case DoS, пов’язаного з async_hooks/ALS, і рекомендує оновитися на пропатчені версії. Команди APM та OTel опублікували гайди з mitigation і контекстом. [4][5]

Нижче — концентроване порівняння пакетів, які реально використовують команди для request context у NestJS. Ціль не «хто має більше зірок», а: коректність, ергономіка, інтеграції та уникнення REQUEST scope.

Швидка таблиця порівняння

PackageCore approachBest forKey trade-offs
nestjs-clsALS + ClsService + Proxy Providers + pluginsfull-featured context + transactionsmore abstraction; learn its API surface
@pas7/nestjs-request-contextALS + typed keys + decorators + adaptersstrict, lightweight, typed request contextnewer ecosystem; depends on your needed integrations
@medibloc/nestjs-request-contextALS-based request contextsimple ALS-based contextnarrower scope vs nestjs-cls
nestjs-pino (context part)logging integration using ALScontextual logging with Pinologging-focused (not a general context framework)
DIY (Nest recipe)raw ALSminimal + full controlyou own edge cases + testing

Далі — деталі з джерелами:

- nestjs-cls позиціонується як continuation-local storage модуль для NestJS на AsyncLocalStorage, наводить сценарії використання: request ID tracking, multi-tenancy, транзакції без прокидування параметрів. [8]

- nestjs-pino документує аргумент, який повторюють багато мейнтейнерів: уникати REQUEST-scoped провайдерів через просідання продуктивності й використовувати ALS для request-bound logger. [11]

- Офіційний рецепт Nest визнає ALS як альтернативу REQUEST scope — але також каже, що NestJS не дає built-in абстракцію, тому ви або імплементите самі, або берете бібліотеку. [2]

Якщо ваші потреби широкі (multi-tenancy + транзакції + proxy providers + «request context там, де REQUEST scope не підтримується»), nestjs-cls часто є найбільш повним toolbox. Документація підкреслює широку матрицю сценаріїв використання й окремо торкається проблематики REQUEST scope. [8]

Частий мотив вибору: потрібна пропагація транзакцій без параметрів + екосистема плагінів для цього патерна. [8]

Якщо ви вже глибоко в цій екосистемі, мігрувати потім зазвичай невигідно — але якщо вам треба лише 2–3 поля (requestId, userId, tenantId), варто розглянути легший варіант.

@pas7/nestjs-request-context напряму відповідає цій проблемі: request context через AsyncLocalStorage із singleton DI-деревом, з фокусом на типобезпеку через typed keys і хорошу NestJS-ергономіку. [10]

Що робить його релевантним саме для цієї статті:

- Typed ContextKey<T> для значень (менше stringly-typed «магічних ключів»). [10]

- NestJS-орієнтована ергономіка: декоратори (наприклад, параметрові) — читання контексту без «проводки». [10]

- HTTP адаптери для Express і Fastify у NestJS. [10]

- Явна позиція «проти REQUEST scope як дефолту» з точки зору перформансу/архітектури. [10]

Застереження: в репозиторії зазначено, що Fastify адаптер поза NestJS має обмеження через несумісності Fastify + AsyncLocalStorage, тому це варто сприймати як NestJS-first рішення. [10]

Це секція «зроби правильно»: де ініціалізувати контекст, що зберігати, як пропагувати в черги/мікросервіси і як тестувати, щоб не відвантажити context leaks.

Рекомендовані поля контексту (практичний дефолт)

- requestId: стабільний correlation ID; echo як x-request-id у відповіді.

- userId: внутрішній ідентифікатор користувача (уникайте email/phone).

- tenantId: якщо multi-tenant.

- traceId / spanId: якщо є tracing (або derive з OTel context).

- authLevel / role: якщо треба для авторизації (у критичних флоу — краще explicit checks).

Pattern A: Minimal DIY ALS (good if you want full control)

Використайте middleware підхід з рецепту Nest і зробіть невеликий wrapper (з тестами). [2]

Pattern B: Use nestjs-cls when you need the ecosystem

Якщо вам потрібні плагіни для транзакцій та proxy provider патерни — nestjs-cls сильний вибір. [8]

Pattern C: Use @pas7/nestjs-request-context when you want strict typed keys + lean surface area

Якщо вам потрібен чистий typed context (requestId/userId/tenantId + ще кілька) і ви хочете NestJS-first adapter layer — бібліотека PAS7 доречна. [10]

Ранній lifecycle

Ініціалізуйте контекст якомога раніше (middleware — найбезпечніше для HTTP у Nest). Офіційний рецепт Nest використовує саме middleware. [2]

Уникайте REQUEST scope

REQUEST-scoped провайдери можуть «розмазати» scope по залежностях і додати накладні витрати на інстанціювання кожного запиту. Nest прямо про це попереджає. [1]

Явна

ALS працює лише in-process. Між сервісами переносіть через headers/metadata. OTel дефолтить на W3C Trace Context (traceparent). [6][7]

Без секретів у контексті

Не пропагуйте чутливі дані в context/baggage. OTel прямо попереджає проти secrets/PII у baggage-подібних полях. [6]

Безпечний потік request context: рання ініціалізація, читання будь-де, явна пропагація через межі

Скріншот секції pas7-quickstart

Нижче — ілюстративні патерни на основі підходу бібліотеки (typed ContextKey<T>, Nest-first ергономіка, адаптери). Перед продакшном звіряйте точні назви API з репозиторієм. [10]

1) Define typed keys (one module/shared package)

TS
import { ContextKey } from '@pas7/nestjs-request-context';

export const REQUEST_ID = new ContextKey<string>('requestId');
export const USER_ID = new ContextKey<string | undefined>('userId');
export const TENANT_ID = new ContextKey<string | undefined>('tenantId');

2) Initialize in middleware (HTTP entry point)

TS
import { Injectable, NestMiddleware } from '@nestjs/common';
import type { Request, Response } from 'express';
import { RequestContextService } from '@pas7/nestjs-request-context';
import { REQUEST_ID, USER_ID, TENANT_ID } from './ctx.keys';

@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    const requestId = String(req.headers['x-request-id'] ?? crypto.randomUUID());
    const userId = req.headers['x-user-id'] ? String(req.headers['x-user-id']) : undefined;
    const tenantId = req.headers['x-tenant-id'] ? String(req.headers['x-tenant-id']) : undefined;

    res.setHeader('x-request-id', requestId);

    RequestContextService.run(() => {
      RequestContextService.set(REQUEST_ID, requestId);
      RequestContextService.set(USER_ID, userId);
      RequestContextService.set(TENANT_ID, tenantId);
      next();
    });
  }
}

3) Read context in services (no param threading)

TS
import { Injectable } from '@nestjs/common';
import { RequestContextService } from '@pas7/nestjs-request-context';
import { REQUEST_ID, USER_ID } from './ctx.keys';

@Injectable()
export class BillingService {
  charge() {
    const requestId = RequestContextService.get(REQUEST_ID);
    const userId = RequestContextService.get(USER_ID);

    return { requestId, userId };
  }
}

Ключовий виграш: сервіси залишаються singleton і чистими, але доступ до контексту на запит зберігається. [10]

ALS не перетинає межі процесів. Якщо ви публікуєте джобу в чергу або викликаєте інший сервіс — контекст треба пропагувати явно (requestId/trace context). [6][7]

Хороший дефолт для пропагації:

- x-request-id (ваш correlation ID)

- traceparent (W3C Trace Context) якщо у вас distributed tracing. [6][7]

HTTP outbound example (pseudo-pattern)

TS
const headers = {
  'x-request-id': RequestContextService.get(REQUEST_ID),
  'traceparent': currentTraceparent,
};

Queue job example (pseudo-pattern)

TS
await queue.add('jobName', {
  data: payload,
  ctx: {
    requestId: RequestContextService.get(REQUEST_ID),
    tenantId: RequestContextService.get(TENANT_ID),
  },
});

На стороні consumer ініціалізуйте новий ALS контекст і відновіть пропаговані поля перед виконанням бізнес-логіки.

Якщо робити request-scoped consumers для джоб — Nest зазначає, що це створює новий інстанс на кожну джобу (схожі компроміси до HTTP request scope). [13]

Context-баги підступні: локально все ніби працює, а під конкурентністю — з’являються витоки.

Мінімальна стратегія тестів:

- Паралельні запити: два одночасні запити не повинні бачити контекст один одного.

- Тести «async boundary»: відкладені задачі (setTimeout, handlers) мають зберігати контекст (або свідомо не зберігати — залежно від дизайну). [3]

- Тести пропагації черги: publish → consume відновлює контекст явно. [6][7]

У бібліотеці PAS7 є тестова історія в репозиторії (і позиціонування через testkit-підхід); якщо робите DIY — будуйте такі ж guardrails. [10]

Якщо потрібне швидке рішення без повторного читання:

  • Обирайте DIY ALS, якщо хочете мінімум залежностей і готові самостійно супроводжувати тести та крайові сценарії. Стартуйте з рецепту Nest. [2]

  • Обирайте nestjs-cls, якщо потрібна зріла екосистема (транзакції, proxy providers, ширші сценарії використання). [8]

  • Обирайте @pas7/nestjs-request-context, якщо хочете lean, type-safe, NestJS-first контекстний шар з адаптерами та чистою ергономікою. [10]

  • Уникайте REQUEST-scoped DI як дефолту: використовуйте лише коли реально потрібна інстанціація на кожен запит і ви контролюєте blast radius. Nest попереджає про performance і cascading scope. [1]

Request context — це «маленьке» архітектурне рішення, яке визначає, чи залишиться код чистим на 30 ендпоінтах або «посиплеться» на 300.

Якщо ви будуєте NestJS продукт і хочете допомоги з backend-архітектурою, observability або автоматизацією — PAS7 Studio може допомогти спроєктувати і реалізувати продакшн-рішення.

Читайте більше: /blog

REQUEST scope у NestJS — це завжди погано?

Ні — але це ризикований дефолт. Nest попереджає, що request-scoped провайдери впливають на продуктивність і можуть каскадно поширювати scope по залежностях. Використовуйте лише коли реально потрібна інстанціація на кожен запит. [1]

AsyncLocalStorage «офіційно підтримується» в NestJS?

NestJS має офіційний рецепт і пояснює, як ALS може бути альтернативою REQUEST-scoped провайдерам, але не постачає built-in абстракцію. [2]

ALS може «магічно» працювати через мікросервіси/черги?

Ні. ALS працює лише in-process. Між сервісами контекст треба пропагувати явно (headers/message metadata). OpenTelemetry дефолтить на W3C Trace Context (`traceparent`). [6][7]

Що викликає втрату ALS контексту?

Node зазначає, що втрата контексту можлива в рідкісних випадках, особливо з callback-based API або кастомними thenables; рекомендовані фікси: promisify або AsyncResource для коректного биндингу контексту. [3]

`@pas7/nestjs-request-context` релевантний і конкурентний для цієї теми?

Так — він таргетить саме цей біль: typed request context поверх ALS із NestJS-first ергономікою та адаптерами. Це сильний варіант, якщо хочете lean, type-safe шар без зсуву DI в REQUEST scope. [10]

Чи треба слідкувати за security-оновленнями Node, якщо використовую ALS?

Так, Node треба тримати пропатченим. У січні 2026 Node випустив mitigation, пов’язаний з async_hooks/ALS екосистемою, а OTel/APM вендори опублікували гайди щодо апдейту. [4][5][14]

Усі лінки нижче напряму стосуються теми та використовувались для фактології й порівнянь вище.

0

Ми публікуємо практичні матеріали про web engineering, автоматизацію, безпеку та продакт-розробку — з фокусом на рішення, які реально важливі в продакшні.

Пов'язані статті

growth

AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти

Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.

blogs

Найпотужніший чіп від Apple? M5 Pro і M5 Max б'ють рекорди

Аналітичний розбір Apple M5 Pro і M5 Max станом на березень 2026 року. Пояснюємо, чому ці чіпи можна вважати найпотужнішими професійними ноутбучними SoC від Apple, як вони виглядають на тлі M4 Pro, M4 Max, M1 Pro, M1 Max і що показують у порівнянні з актуальними Intel та AMD.

telegram-media-saver

Автоматичне тегування та пошук збережених посилань

Інтеграція з GDrive/S3/Notion для автоматичного тегування та швидкого пошуку через пошукові API

services

Розробка Telegram-ботів та автоматизація

Професійна розробка Telegram-ботів та автоматизація бізнес-процесів: чат-боти, AI-асистенти, інтеграції з CRM та автоматизація процесів.

Веб-розробка для вашого бізнесу

Професійна розробка сучасних веб-додатків та сайтів

Розробка сайтів під ключ

Створення сайтів для бізнесу: лендінги, корпоративні сайти та інтернет-магазини з інтеграціями, високою швидкістю і SEO-ready структурою.

Професійна розробка для вашого бізнесу

Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.