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

Що ви отримаєте з цієї статті
Це не «пост про теорію». Це практичний гайд, який можна застосувати в реальному 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]
Найболючіша проблема NestJS: request context без побічних руйнувань
«Request context» — це все, що ви хочете читати глибоко в сервісах без прокидування через кожен метод: поточний user, tenant, permissions, request ID, trace IDs, handle транзакції, locale, feature flags тощо.
У NestJS ви зазвичай приходите до одного з цих варіантів:
- Ви прокидуєте контекст параметрами всюди (надійно, але шумно й складно підтримувати).
- Ви використовуєте REQUEST-scoped провайдери (простий API, але це може зробити значну частину застосунку такою, що створюється на кожен запит). [1]
Чому REQUEST-scoped DI стає пасткою (і чому команди шкодують пізніше)
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]
AsyncLocalStorage за одну хвилину: чому воно ідеально лягає на request context
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 scope
NestJS прямо позиціонує ALS як спосіб прокидати стан без параметрів і як альтернативу REQUEST-scoped провайдерам «та деяким їх обмеженням». [2]
Ключова ідея: обгорнути запит якнайраніше (middleware) через als.run(store, () => next()). Тоді будь-який провайдер може читати зі store пізніше.
Мінімальний NestJS-приклад (спрощено з офіційного рецепту): [2]
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 та тести.
Як роблять компанії: correlation IDs + tracing + безпечна пропагація
У продакшні 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]
Ландшафт пакетів (2026): що існує, що надійне, що ризиковане
Нижче — концентроване порівняння пакетів, які реально використовують команди для request context у NestJS. Ціль не «хто має більше зірок», а: коректність, ергономіка, інтеграції та уникнення REQUEST scope.
Швидка таблиця порівняння
| Package | Core approach | Best for | Key trade-offs |
|---|---|---|---|
nestjs-cls | ALS + ClsService + Proxy Providers + plugins | full-featured context + transactions | more abstraction; learn its API surface |
@pas7/nestjs-request-context | ALS + typed keys + decorators + adapters | strict, lightweight, typed request context | newer ecosystem; depends on your needed integrations |
@medibloc/nestjs-request-context | ALS-based request context | simple ALS-based context | narrower scope vs nestjs-cls |
nestjs-pino (context part) | logging integration using ALS | contextual logging with Pino | logging-focused (not a general context framework) |
| DIY (Nest recipe) | raw ALS | minimal + full control | you 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]
Deep dive: коли `nestjs-cls` — найкращий вибір
Якщо ваші потреби широкі (multi-tenancy + транзакції + proxy providers + «request context там, де REQUEST scope не підтримується»), nestjs-cls часто є найбільш повним toolbox. Документація підкреслює широку матрицю сценаріїв використання й окремо торкається проблематики REQUEST scope. [8]
Частий мотив вибору: потрібна пропагація транзакцій без параметрів + екосистема плагінів для цього патерна. [8]
Якщо ви вже глибоко в цій екосистемі, мігрувати потім зазвичай невигідно — але якщо вам треба лише 2–3 поля (requestId, userId, tenantId), варто розглянути легший варіант.
Де підходить `@pas7/nestjs-request-context` (і коли це конкурентно)
@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]
Точка ініціалізації
Ініціалізуйте контекст якомога раніше (middleware — найбезпечніше для HTTP у Nest). Офіційний рецепт Nest використовує саме middleware. [2]
Singleton DI
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: рання ініціалізація, читання будь-де, явна пропагація через межі
Section pas7-quickstart screenshotCode patterns: `@pas7/nestjs-request-context` (typed keys + ергономічний доступ)
Нижче — ілюстративні патерни на основі підходу бібліотеки (typed ContextKey<T>, Nest-first ергономіка, адаптери). Перед продакшном звіряйте точні назви API з репозиторієм. [10]
1) Define typed keys (one module/shared package)
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)
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)
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)
HTTP outbound example (pseudo-pattern)
const headers = {
'x-request-id': RequestContextService.get(REQUEST_ID),
'traceparent': currentTraceparent,
};Queue job example (pseudo-pattern)
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 leaks у продакшн
Context-баги підступні: локально все ніби працює, а під конкурентністю — з’являються витоки.
Мінімальна стратегія тестів:
- Паралельні запити: два одночасні запити не повинні бачити контекст один одного.
- Тести «async boundary»: відкладені задачі (setTimeout, handlers) мають зберігати контекст (або свідомо не зберігати — залежно від дизайну). [3]
У бібліотеці 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]
Якщо ви будуєте серйозний NestJS продукт
Request context — це «маленьке» архітектурне рішення, яке визначає, чи залишиться код чистим на 30 ендпоінтах або «посиплеться» на 300.
Якщо ви будуєте NestJS продукт і хочете допомоги з backend-архітектурою, observability або автоматизацією — PAS7 Studio може допомогти спроєктувати і реалізувати продакшн-рішення.
Читайте більше: https://pas7.com.ua/blog
Джерела та перехресні посилання
Усі лінки нижче напряму стосуються теми та використовувались для фактології й порівнянь вище.
• 1. NestJS docs — Injection scopes (REQUEST scope behavior, cascading scope, performance notes) Read source ↗
• 2. NestJS docs — Async Local Storage recipe (official ALS approach and rationale) Read source ↗
• 3. Node.js docs — AsyncLocalStorage & troubleshooting context loss (official behavior and recommendations) Read source ↗
• 4. Node.js blog — DoS mitigation advisory related to async_hooks/AsyncLocalStorage ecosystem reliance (Jan 2026) Read source ↗
• 5. OpenTelemetry — JS statement on Node.js DoS mitigation (Jan 2026) Read source ↗
• 6. OpenTelemetry docs — Context propagation concepts, security notes, and W3C Trace Context default Read source ↗
• 7. W3C — Trace Context specification (traceparent header format standard) Read source ↗
• 8. nestjs-cls — official documentation (use cases and ALS-based approach) Read source ↗
• 10. PAS7 Studio — @pas7/nestjs-request-context repository (typed keys, adapters, ergonomics, limitations) Read source ↗
• 11. nestjs-pino — repository (ALS-based contextual logging argument vs REQUEST scope) Read source ↗
• 12. Medibloc — nestjs-request-context repository (ALS-based request context library) Read source ↗
• 13. NestJS docs — Queues (request-scoped consumers instantiation per job) Read source ↗
• 14. Datadog — mitigation guidance referencing ALS/APM impact (Jan 2026) Read source ↗
FAQ
Ні — але це ризикований дефолт. Nest попереджає, що request-scoped провайдери впливають на продуктивність і можуть каскадно поширювати scope по залежностях. Використовуйте лише коли реально потрібна інстанціація на кожен запит. [1]
NestJS має офіційний рецепт і пояснює, як ALS може бути альтернативою REQUEST-scoped провайдерам, але не постачає built-in абстракцію. [2]
Ні. ALS працює лише in-process. Між сервісами контекст треба пропагувати явно (headers/message metadata). OpenTelemetry дефолтить на W3C Trace Context (`traceparent`). [6][7]
Node зазначає, що втрата контексту можлива в рідкісних випадках, особливо з callback-based API або кастомними thenables; рекомендовані фікси: promisify або AsyncResource для коректного биндингу контексту. [3]
Так — він таргетить саме цей біль: typed request context поверх ALS із NestJS-first ергономікою та адаптерами. Це сильний варіант, якщо хочете lean, type-safe шар без зсуву DI в REQUEST scope. [10]
Так, Node треба тримати пропатченим. У січні 2026 Node випустив mitigation, пов’язаний з async_hooks/ALS екосистемою, а OTel/APM вендори опублікували гайди щодо апдейту. [4][5][14]
Хочете більше глибоких, підтверджених джерелами інженерних розборів?
Ми публікуємо практичні матеріали про web engineering, автоматизацію, безпеку та продакт-розробку — з фокусом на рішення, які реально важливі в продакшні.