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 Feb 2026· 18 min read
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]

Найболючіша проблема NestJS: request context без побічних руйнувань

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

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

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

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

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

Чому 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]

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 та тести.

Як роблять компанії: 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.

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

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]

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]

Точка ініціалізації

Ранній lifecycle

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

Singleton DI

Уникайте 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]

Diagram: request enters middleware -> ALS store -> services -> logger -> db -> queue propagation

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

Section pas7-quickstart screenshot

Code patterns: `@pas7/nestjs-request-context` (typed keys + ергономічний доступ)

Нижче — ілюстративні патерни на основі підходу бібліотеки (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 leaks у продакшн

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]

Якщо ви будуєте серйозний NestJS продукт

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

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

Читайте більше: https://pas7.com.ua/blog

Джерела та перехресні посилання

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

FAQ

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]

Хочете більше глибоких, підтверджених джерелами інженерних розборів?

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

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

growth15 лютого 2026 р.

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

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

Читати →
telegram-media-saver8 січня 2025 р.

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

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

Читати →
services1 січня 2025 р.

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

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

Читати →
backend-engineering15 лютого 2026 р.

Bun vs Node.js у 2026: чому Bun відчувається швидшим (і як перевірити застосунок перед міграцією)

Bun — це швидший all-in-one JavaScript toolkit: runtime, пакетний менеджер, бандлер і тест-раннер. Розбираємо, що реально дає приріст (з бенчмарками), що може зламатися, і як отримати безкоштовний readiness-аудит через @pas7-studio/bun-ready.

Читати →

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

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

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

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

Детальніше →

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

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