Все уроки
БэкендСредний14 мин чтенияВ моём стеке

Node.js и NestJS

JavaScript на сервере — с архитектурой, которая масштабируется на большие команды.

  • Node.js 22
  • Event loop
  • NestJS 11
  • Prisma
  • Electron
Шаг 1/5
Приходит запрос: POST /orders. Router смотрит на URL и метод и выбирает action контроллера, который его обработает.

За 30 секунд

Node.js выполняет JavaScript на сервере в одном основном потоке с event loop, поэтому он очень хорошо справляется с множеством одновременных сетевых запросов. NestJS — это TypeScript-фреймворк поверх Node, который добавляет модули, внедрение зависимостей, guards и pipes, чтобы большие бэкенды оставались упорядоченными. Prisma — типобезопасный инструмент для работы с базой данных, который часто используют с обоими.

Почему я это использую

  • Я использую Node, когда весь продукт написан на TypeScript: тогда типы и валидацию можно разделять между API, вебом, десктопом и мобильным приложением.
  • Я использую NestJS, потому что модули и внедрение зависимостей сохраняют порядок в большом мультитенантном бэкенде с десятками доменов.
  • Я использую Node для realtime и задач с интенсивным I/O (сокеты, очереди, вебхуки), где его неблокирующая модель раскрывается лучше всего.
  • Я использую ту же среду выполнения внутри Electron, поэтому десктопная касса может разделять код с сервером.

Как это работает

01Event loop простыми словами

Представьте официанта, который в одиночку обслуживает много столиков. Он принимает заказ, передаёт его на кухню и сразу идёт к следующему столику, а не стоит в ожидании. Когда блюдо готово, кухня его зовёт. Node работает так же. Ваш JavaScript выполняется в одном основном потоке. Когда вы читаете файл, делаете запрос в базу или вызываете API, Node передаёт работу операционной системе или небольшому пулу потоков (libuv) и продолжает выполнять другой код. Когда результат готов, колбэк или разрешённый Promise ставится в очередь, и event loop его выполняет. Это и есть неблокирующий I/O. Подвох в том, что если ваш собственный код делает тяжёлую работу на CPU — например, ресайз изображений или парсинг огромного JSON, — официант застревает на кухне, и все остальные запросы ждут.

  • Фазы: timers, pending callbacks, poll (I/O), check (setImmediate), close callbacks.
  • Микрозадачи (колбэки Promise, queueMicrotask) выполняются между шагами, до следующей макрозадачи.
  • Правило: никогда не блокируйте цикл; переносите работу на CPU в worker threads или очередь.

02Когда подходит Node и какие версии актуальны в конце 2026

Node отлично подходит для API, которые в основном ждут базы данных и другие сервисы, для realtime-функций с множеством открытых соединений, BFF (слоёв backend-for-frontend), вебхуков, CLI и инструментов разработки. Хуже он подходит для тяжёлых вычислений на CPU, где лучше справляются Go, Rust или отдельный сервис-воркер. Для продакшена используйте LTS-релиз: на ноябрь 2026 года Node 24 находится в maintenance LTS до апреля 2028, у Node 22 конец жизни в апреле 2027, а Node 26 только что перешёл в LTS. В современном Node есть fetch, встроенный тест-раннер (node:test), watch-режим и нативное удаление TypeScript-типов для простых случаев, так что дополнительных пакетов нужно меньше, чем несколько лет назад.

03Express или NestJS

Express минималистичен: роутер и middleware, больше ничего. Для небольшого сервиса это идеально, но в большой кодовой базе каждая команда изобретает свою структуру. NestJS даёт эту структуру готовой. Модуль объединяет один домен (заказы, меню, лояльность) с его контроллерами и провайдерами. Внедрение зависимостей означает, что класс объявляет в конструкторе, что ему нужно, а Nest сам его собирает, — поэтому подменить реальный сервис фейком в тестах проще простого. Вокруг каждого запроса Nest выполняет фиксированный конвейер: middleware, затем guards (может ли этот пользователь это вызвать?), затем interceptors (логирование, замеры времени, преобразование ответа), затем pipes (валидация и трансформация ввода), затем ваш обработчик и, наконец, exception filters, которые превращают ошибки в аккуратные HTTP-ответы. По умолчанию NestJS работает на Express, а для большей пропускной способности — на Fastify.

ts
@Controller('orders')
@UseGuards(JwtAuthGuard, RolesGuard)
export class OrdersController {
  constructor(private readonly orders: OrdersService) {}
 
  @Post()
  @Roles('cashier', 'manager')
  create(@CurrentUser() user: AuthUser, @Body() dto: CreateOrderDto) {
    return this.orders.create(user.companyId, dto);
  }
}
 
// main.ts
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

04Prisma как слой данных

Prisma позволяет описать таблицы в файле схемы и генерирует полностью типизированный клиент: если колонка nullable, TypeScript об этом знает. Миграции генерируются из изменений схемы командой prisma migrate dev и применяются в продакшене через prisma migrate deploy. Используйте select или include, чтобы получать только нужное, $transaction для многошаговых записей и client extensions для сквозных правил — например, чтобы каждый запрос обязательно содержал id тенанта. Начиная с Prisma 7 настройки подключения живут в prisma.config.ts, а клиент создаётся с драйвер-адаптером (например, @prisma/adapter-pg), поэтому перед переносом старого проекта загляните в гайд по обновлению. И помните: Prisma — не магия. Проверяйте сгенерированный SQL на медленные запросы и добавляйте индексы в схему.

ts
const orders = await prisma.order.findMany({
  where: { companyId, status: 'PAID', createdAt: { gte: since } },
  select: {
    id: true,
    total: true,
    customer: { select: { id: true, name: true } },
  },
  orderBy: { createdAt: 'desc' },
  take: 50,
});

05Worker threads, дочерние процессы и Electron

Если в Node всё-таки нужно выполнять работу на CPU, уберите её из основного потока. Worker threads (node:worker_threads) выполняют JavaScript в параллельных потоках со своим event loop и общаются через сообщения; используйте библиотеку пула, а не создавайте поток на каждый запрос. Для долгой или повторяемой работы обычно лучше очередь с отдельными процессами-воркерами (BullMQ), потому что она переживает перезапуски. Node API масштабируют горизонтально, запуская несколько процессов (cluster-режим pm2, контейнеры) за балансировщиком нагрузки. Electron — это Node плюс Chromium в одном десктопном приложении: у main-процесса есть доступ к Node, окна-рендереры показывают ваш React UI, а preload-скрипт открывает между ними небольшой безопасный API. В рендерерах держите contextIsolation включённым, а nodeIntegration — выключенным.

06Советы для продакшена

Валидируйте весь ввод на входе через DTO (class-validator) или zod. Обрабатывайте ошибки через exception filters и никогда не оставляйте отклонённый Promise без обработки. Пишите логи в структурированном JSON с request id. Реализуйте graceful shutdown: по SIGTERM перестаньте принимать трафик, завершите текущие запросы, закройте соединения с базой и Redis. Добавьте health checks для балансировщика. Ставьте таймауты на каждый исходящий HTTP-вызов. Держите зависимостей немного и проверяйте их, фиксируйте версию Node в .nvmrc или engines и собирайте в Docker-образах чистый JavaScript, а не запускайте TypeScript через dev-инструмент в продакшене.

  • app.enableShutdownHooks() в NestJS, чтобы срабатывали хуки onModuleDestroy.
  • Отслеживайте задержку event loop как метрику: если она растёт, что-то блокирует цикл.

Когда применять

  • Full-stack продукты на TypeScript, где общие типы для веба, мобильного приложения и сервера экономят время.
  • Realtime-функции и API с интенсивным I/O и множеством одновременных соединений.
  • Большие бэкенды с множеством доменов, где модули и DI в NestJS поддерживают единообразие между командами.
  • Десктопные приложения (Electron) или инструменты, которые должны разделять код с сервером.

Когда не стоит

  • Нагрузки, упирающиеся в CPU, — кодирование видео или тяжёлые вычисления как основная задача сервиса.
  • Команда, которая уже продуктивна в Laravel или Django на CRUD-продукте, — переход даст мало.
  • Совсем небольшие сервисы, где структура NestJS избыточна; хватит чистого Fastify или Hono.

Альтернативы

  • Laravel

    PHP с «батарейками в комплекте»; быстрее для классического CRUD и продуктов с большой админкой.

  • Fastify / Hono

    Лёгкие Node-фреймворки, когда нужна скорость без структуры Nest.

  • Go

    Компилируемый язык, отличная конкурентность и производительность на CPU; для бизнес-логики многословнее.

  • Bun / Deno

    Альтернативные среды выполнения JavaScript; перед продакшеном проверьте совместимость библиотек.

Лучшие практики

  1. Никогда не блокируйте event loop; переносите работу на CPU или медленные операции в worker threads или очереди.
  2. Используйте глобальный ValidationPipe с whitelist, чтобы неизвестные поля отбрасывались.
  3. Держите один модуль Nest на домен и экспортируйте только то, что нужно другим модулям.
  4. Размещайте авторизацию в guards, а не в разбросанных по коду if.
  5. Используйте select в Prisma, чтобы не тянуть лишнее, и транзакции для многошаговых записей.
  6. Реализуйте graceful shutdown и health checks до выхода в продакшен.
  7. Работайте на LTS-версии Node и планируйте обновления до окончания её поддержки.

Частые ошибки

  • Синхронная криптография, обработка изображений или большие циклы прямо в обработчиках запросов.
  • Забытый await, из-за чего ошибки превращаются в необработанные rejection, а данные записываются наполовину.
  • Циклические зависимости модулей в NestJS, замазанные forwardRef повсюду.
  • Создание нового PrismaClient на каждый запрос, которое исчерпывает соединения с базой.
  • Доступ окон-рендереров Electron к Node API при включённом nodeIntegration.

Глубже

07Как измерить заблокированный event loop и пул потоков libuv

Нельзя исправить блокировку, которую не видно. В Node есть monitorEventLoopDelay в node:perf_hooks: он замеряет, насколько поздно цикл выполняет таймеры, и растущий p99 означает, что какой-то синхронный код занимает поток. Экспортируйте его как метрику рядом с задержкой запросов. Чтобы найти виновника, запишите CPU-профиль через node --cpu-prof под нагрузкой и откройте файл .cpuprofile в Chrome DevTools; самые широкие полосы — ваши горячие функции, часто это JSON.parse на огромных payload, синхронная криптография или регулярное выражение с катастрофическим бэктрекингом. Второе, более тихое узкое место — пул потоков libuv, в котором по умолчанию всего 4 потока. Вызовы файловой системы, dns.lookup, zlib, pbkdf2 и scrypt и многие нативные модули хэширования делят его между собой, поэтому 50 одновременных хэширований паролей встают в очередь друг за другом. Увеличение UV_THREADPOOL_SIZE при старте помогает, но настоящее решение — вынести тяжёлую работу в пул воркеров.

ts
import { monitorEventLoopDelay } from 'node:perf_hooks';
 
const loop = monitorEventLoopDelay({ resolution: 20 });
loop.enable();
 
setInterval(() => {
  const p99ms = loop.percentile(99) / 1e6; // nanoseconds to milliseconds
  metrics.gauge('event_loop_delay_p99_ms', p99ms);
  if (p99ms > 200) logger.warn({ p99ms }, 'event loop is blocked');
  loop.reset();
}, 10_000).unref();

08Контекст запроса через AsyncLocalStorage

В PHP каждый запрос — отдельный процесс, поэтому «текущий пользователь» — это просто. В Node тысячи запросов перемежаются в одном потоке, поэтому глобальная переменная их перепутала бы. AsyncLocalStorage из node:async_hooks решает эту задачу: вы открываете контекст в начале запроса, и каждая функция, вызванная из него, через await, таймеры и цепочки Promise, может читать одно и то же хранилище. Это идеально для request id в каждой строке лога, tenant id для расширения Prisma или trace id для исходящих вызовов. В NestJS это аккуратно оборачивает пакет nestjs-cls. Два правила: держите хранилище маленьким и почти неизменяемым и всегда открывайте его в middleware, а не в guard, чтобы всё ниже по цепочке его унаследовало. Работа, запущенная вне запроса, например воркер очереди, требует своего вызова run() на каждую задачу.

ts
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
 
type Ctx = { requestId: string; companyId?: string };
export const requestContext = new AsyncLocalStorage<Ctx>();
 
// main.ts (Express adapter)
app.use((req, _res, next) => {
  const requestId = req.headers['x-request-id']?.toString() ?? randomUUID();
  requestContext.run({ requestId }, next);
});
 
// Anywhere deeper, even after several awaits
logger.info({ requestId: requestContext.getStore()?.requestId }, 'order created');

09Injection scopes в NestJS и их скрытая цена

По умолчанию каждый провайдер NestJS — singleton: создаётся один раз при старте и используется всеми, что быстро и обычно правильно. Scope.REQUEST создаёт новый экземпляр на каждый запрос, чтобы можно было внедрить объект запроса. Подвох в том, что request scope всплывает вверх: любой контроллер или провайдер, зависящий от request-scoped провайдера, тоже становится request-scoped, поэтому одно невинное внедрение может заставить всё дерево зависимостей пересобираться на каждый вызов, добавляя задержку и нагрузку на сборщик мусора. Предпочитайте singletons, которые получают контекст аргументами методов или читают его из AsyncLocalStorage. Если вам действительно нужны экземпляры на тенант, например отдельное соединение на тенант, NestJS предлагает durable providers с ContextIdStrategy, которые держат одно поддерево на тенант, а не на запрос. Динамические модули с forRootAsync и ConfigService — чистый способ собирать настраиваемые singletons при старте.

10Память, стримы и соединения с базой

Куча Node ограничена, поэтому загрузка экспорта на 500 MB в массив роняет процесс с «JavaScript heap out of memory». Стримы обрабатывают данные маленькими порциями, а stream.pipeline соединяет их с backpressure: если получатель медленный, источник встаёт на паузу, а не буферизует всё. Асинхронные генераторы — это читаемые источники, поэтому можно листать базу страницами и сразу писать строки CSV. Для поиска утечек запустите процесс с --heapsnapshot-signal=SIGUSR2, снимите два снимка с разницей в несколько минут и сравните в DevTools, что выросло; типичные виновники — неограниченные кэши в памяти, никогда не удаляемые слушатели и замыкания, удерживающие большие объекты. Соединения — второй ограниченный ресурс. Каждый процесс должен создавать один клиент Prisma; с driver adapters в Prisma 7 размер пула задаётся на драйвере, например max у pg Pool. Умножьте размер пула на число процессов и держите сумму ниже лимита базы или поставьте перед ней PgBouncer.

ts
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';
import { createWriteStream } from 'node:fs';
 
async function* ordersAsCsv(companyId: string) {
  yield 'id,total,created_at\n';
  let cursor: string | undefined;
  do {
    const page = await prisma.order.findMany({
      where: { companyId }, take: 1000, orderBy: { id: 'asc' },
      ...(cursor && { cursor: { id: cursor }, skip: 1 }),
    });
    for (const o of page) yield o.id + ',' + o.total + ',' + o.createdAt.toISOString() + '\n';
    cursor = page.at(-1)?.id;
  } while (cursor);
}
 
await pipeline(ordersAsCsv(companyId), createGzip(), createWriteStream('/tmp/orders.csv.gz'));

Реальные ситуации

01

Всплески задержки каждые несколько минут без видимой причины

Проблема

API обычно отвечает за 40 мс, но каждые несколько минут все запросы разом занимают две-три секунды. CPU в среднем выглядит нормально, база простаивает.

Как решить

Когда всё замедляется одновременно, это указывает на заблокированный event loop, а не на медленную зависимость. Добавьте метрику задержки event loop и сопоставьте всплески с логами: часто в этот момент синхронно выполняется эндпоинт отчёта, большой JSON-экспорт или массовый импорт. Воспроизведите это под нагрузкой и запишите node --cpu-prof, чтобы найти горячую функцию. Затем уберите эту работу с потока запросов: стримьте экспорт, разбейте импорт на страницы или выполняйте CPU-часть в пуле worker threads вроде piscina. Если работа долгая или должна переживать рестарты, положите её в очередь BullMQ и верните job id, который клиент сможет опрашивать.

02

Postgres жалуется на слишком много соединений после масштабирования

Проблема

Трафик вырос, и команда перешла с 2 на 10 контейнеров API. Теперь в пиках Postgres отклоняет новые соединения, и часть запросов падает с ошибками подключения.

Как решить

Посчитайте: контейнеры умножить на процессы в контейнере умножить на размер пула. Десяти контейнерам с пулом 20 уже нужно 200 соединений, плюс воркеры и миграции. Сначала проверьте, что каждый процесс создаёт ровно один клиент Prisma, а не по одному на запрос или модуль. Затем задайте размеры пулов осознанно, часто 5–10 на процесс, и подтвердите метриками, что запросы не ждут пул. Если процессов всё равно нужно больше, поставьте PgBouncer в режиме transaction между приложениями и Postgres, чтобы тысячи клиентских соединений делили небольшое число серверных, и проверьте рекомендации своего ORM для работы за пулером.

03

Каждый деплой теряет несколько запросов

Проблема

Каждый rolling deploy даёт всплеск ошибок 502 на балансировщике. Пользователи иногда видят экран неудачной оплаты, хотя в коде ничего не менялось.

Как решить

Старый процесс убивают, пока он ещё держит соединения. Правильно обрабатывайте SIGTERM: вызовите app.enableShutdownHooks() в NestJS, сначала заставьте readiness-эндпоинт падать, чтобы балансировщик перестал слать новый трафик, подождите несколько секунд, затем дайте HTTP-серверу закрыться и завершить текущие запросы, прежде чем закрывать Prisma и Redis. Дайте оркестратору grace period длиннее вашего самого медленного запроса. Вторая причина — keep-alive: keepAliveTimeout в Node по умолчанию 5 секунд, это меньше idle timeout многих балансировщиков, поэтому балансировщик переиспользует сокет, который Node только что закрыл. Задайте keepAliveTimeout больше idle timeout балансировщика, а headersTimeout чуть больше него.

Шпаргалка

Сгенерировать полный CRUD-модуль в NestJS
npx nest g resource orders
Записать CPU-профиль
node --cpu-prof dist/main.js
Снимать heap snapshots по требованию
node --heapsnapshot-signal=SIGUSR2 dist/main.js # then: kill -USR2 <pid>
Загрузить файл .env без пакета
node --env-file=.env dist/main.js
Запустить встроенный тест-раннер в watch-режиме
node --test --watch
Дать libuv больше потоков
UV_THREADPOOL_SIZE=16 node dist/main.js
Применить миграции Prisma в продакшене
npx prisma migrate deploy
Переключить NestJS на Fastify
NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter())
Keep-alive дольше 60 с балансировщика
server.keepAliveTimeout = 65_000; server.headersTimeout = 66_000;

В моих реальных проектах

  • Был одним из лидов мультитенантного POS SaaS для ресторанов на NestJS 11, Prisma 6 и PostgreSQL: 83 доменных модуля, около 936 обработчиков маршрутов и 181 модель Prisma, построено примерно за четыре месяца основной командой из двух человек.
  • Изоляция тенантов там обеспечивается дважды: явный companyId в каждом запросе плюс расширение Prisma, которое бросает ошибку, если скоуп не указан.
  • Касса для персонала работает как десктопное Electron-приложение с автообновлением, а отдельный процесс-воркер обрабатывает очереди BullMQ.
  • В кодовой базе около 270 тестовых файлов на встроенном раннере node:test.

Вопросы и ответы

Q1Node однопоточный. Как он обрабатывает тысячи запросов?

В одном потоке выполняется только JavaScript. Сетевой I/O передаётся неблокирующим механизмам операционной системы, а часть работы — доступ к файлам, DNS и криптография — уходит в пул потоков libuv. Пока эти операции выполняются, event loop продолжает принимать и обрабатывать другие запросы. Когда результаты готовы, их колбэки или продолжения Promise ставятся в очередь и выполняются. Поэтому тысячи запросов, ожидающих базу данных, стоят очень дёшево. Ограничение — CPU: любая синхронная тяжёлая работа блокирует все запросы. Для неё используют worker threads, очередь или больше процессов за балансировщиком.

Q2В чём разница между process.nextTick, Promise и setImmediate?

Все три откладывают работу на потом, но в разные моменты. Колбэки process.nextTick выполняются сразу после текущей операции, раньше других микрозадач. Колбэки Promise — это микрозадачи; они выполняются, как только опустеет текущий стек вызовов, до того как event loop пойдёт дальше. setImmediate — макрозадача, которая выполняется в фазе check, после опроса I/O в текущей итерации цикла. setTimeout с 0 выполняется в фазе timers одной из следующих итераций. На практике: предпочитайте Promise, используйте setImmediate, чтобы во время долгой работы уступить место I/O, и избегайте рекурсивного nextTick, потому что он может «заморить» цикл.

Q3Почему NestJS, а не чистый Express?

Express даёт роутинг и middleware, а архитектуру оставляет на вас. Для небольшого сервиса это нормально, но в большой кодовой базе каждый разработчик строит структуру по-своему. NestJS её стандартизирует: модули по доменам, внедрение зависимостей для тестируемости, guards для авторизации, pipes для валидации, interceptors для сквозных задач и exception filters для единообразных ошибок. Новые разработчики знают, где что лежит. Ещё он аккуратно интегрируется с OpenAPI, очередями, вебсокетами и транспортами микросервисов. Цена — больше шаблонного кода и порог входа с декораторами и DI, поэтому для крошечного сервиса я всё равно выбрал бы что-то полегче.

Q4Объясните guards, pipes и interceptors в NestJS.

Они работают на разных стадиях запроса. Guards выполняются первыми после middleware и отвечают на один вопрос: может ли этот запрос продолжиться? Их используют для проверки аутентификации, ролей и прав; они возвращают true или бросают исключение. Interceptors оборачивают обработчик, поэтому могут действовать до и после него: логирование, замеры времени, кэширование, преобразование формы ответа или добавление таймаута. Pipes трансформируют и валидируют аргументы прямо перед тем, как их получит обработчик, — например, ValidationPipe превращает JSON-тело в провалидированный DTO, а ParseIntPipe конвертирует параметр маршрута. Затем exception filters перехватывают ошибки и форматируют HTTP-ответ.

Q5Когда вы используете worker threads?

Когда Node-сервису нужно выполнить тяжёлую для CPU работу, которая заблокировала бы event loop: обработку изображений или PDF, сжатие, парсинг больших файлов, сложные вычисления. Worker threads выполняют JavaScript параллельно, со своим event loop, и общаются через сообщения или общую память. Я использую пул, а не создаю поток на каждый запрос, потому что создание потоков не бесплатно. Если работа долгая, должна переживать перезапуски или требует повторных попыток, я предпочитаю очередь вроде BullMQ с отдельными процессами-воркерами. С I/O worker threads не помогают — Node и так обрабатывает его без блокировки.

Q6Как не допустить утечки данных между тенантами в бэкенде на Node и Prisma?

Сделать так, чтобы забыть фильтр по тенанту было сложно. Во-первых, каждый метод сервиса явно получает id тенанта из аутентифицированного контекста, никогда — из тела запроса, и каждый запрос его содержит. Во-вторых, добавить страховку: расширение клиента Prisma или middleware, которое проверяет запросы к моделям, принадлежащим тенанту, и бросает ошибку, если условия по тенанту нет, с коротким allow-list для обоснованных исключений. Для записей другого тенанта возвращать 404, а не 403, чтобы не подтверждать существование id. И наконец, для каждого модуля писать тест с «чужим» тенантом. Row-level security в PostgreSQL — дополнительный слой, который стоит рассмотреть.

Вопросы уровня senior

S1Как бы вы подняли Node API с 1,000 до 20,000 запросов в секунду?

Сначала нахожу узкое место нагрузочными тестами и профилями, а не догадками. Сам Node хорошо держит высокую конкурентность I/O, поэтому пределы обычно — CPU на процесс, база данных и внешние вызовы. Я запускаю по процессу на ядро, через контейнеры или cluster mode, за балансировщиком, сохраняя процессы stateless: сессии и кэши в Redis. Тяжёлую по CPU работу выношу в пулы воркеров или очереди и слежу за задержкой event loop. На стороне данных: индексы, меньше round trips, кэширование горячих чтений с понятной инвалидацией, read-реплики и пулы соединений, подобранные под лимит базы. Переключение NestJS на адаптер Fastify и сокращение JSON-payload дают измеримый выигрыш. Компромисс — стоимость и сложность, поэтому я останавливаюсь, когда целевая p99-задержка достигнута с запасом.

S2Как вы проносите контекст тенанта и запроса через приложение на NestJS?

Три варианта с понятными компромиссами. Явная передача контекста аргументами — самая прозрачная и тестируемая, и для tenant id в методах сервисов я делаю так всегда. Request-scoped провайдеры могут внедрять запрос, но scope всплывает по дереву зависимостей и пересоздаёт много объектов на каждый запрос, что стоит задержки. AsyncLocalStorage, часто через nestjs-cls, даёт каждому запросу неявное хранилище, переживающее await, и это подходит для сквозных данных: request id для логов, trace id и страховочное расширение Prisma, проверяющее фильтр по тенанту. Риск — скрытая связанность, поэтому я ограничиваю то, что попадает в хранилище, задаю его только в middleware и всё равно передаю критичные для бизнеса значения явно. Задачи очереди открывают свой контекст из payload задачи.

S3Как найти и исправить утечку памяти в продакшен-сервисе на Node?

Сначала подтвердите её: память стабильно растёт часами и не падает после сборки мусора, часто заканчиваясь рестартами по out-of-memory. Постройте график используемой кучи на процесс рядом с трафиком, чтобы исключить обычную нагрузку. Затем воспроизведите на staging при похожем трафике, запустите процесс с --heapsnapshot-signal, снимите снимки с интервалом в несколько минут и через режим сравнения в DevTools посмотрите, какие типы объектов растут и что их удерживает. Частые причины — неограниченные Map в роли кэшей, слушатели событий, добавляемые на каждый запрос, никогда не очищаемые таймеры и большие замыкания, удерживаемые долгоживущими объектами. Устраните корневую причину, например LRU с ограничением размера. Периодический перезапуск воркеров — страховка, а не решение, потому что он скрывает и следующую утечку.

S4Когда бы вы разделили монолит на NestJS на микросервисы?

Когда у модуля действительно другая потребность: он должен масштабироваться независимо, деплоиться по своему графику, принадлежать отдельной команде или изолировать риск, как интеграция платежей. Без одной из этих причин микросервисы добавляют сетевые сбои, распределённые транзакции, версионируемые контракты и больше инфраструктуры без всякой выгоды. NestJS делает путь глаже, потому что у модулей уже есть чёткие границы, а у фреймворка есть транспорты микросервисов для Redis, NATS, RabbitMQ, Kafka и gRPC. Сначала я делаю так, чтобы модуль общался с остальным только через свой публичный сервис и события, затем переношу его за транспорт, оставаясь в одном репозитории, и только потом деплою отдельно. Если модуль нельзя изолировать внутри процесса, сервисом он тоже не заработает.

Глоссарий

libuv
Библиотека на C под Node, которая даёт event loop, неблокирующий сетевой I/O и небольшой пул потоков для остальной работы.
Задержка event loop
Насколько поздно цикл выполняет запланированные колбэки. Высокая задержка значит, что синхронный код блокирует каждый запрос.
Microtask
Колбэк, например продолжение Promise, который выполняется сразу после текущего кода, раньше таймеров и I/O.
Provider
Любой класс, который NestJS может внедрить: сервисы, репозитории, фабрики или значения, зарегистрированные в модуле.
Injection scope
Сколько живёт экземпляр провайдера NestJS: singleton по умолчанию, на запрос или transient на каждое внедрение.
Dynamic module
Модуль NestJS, настраиваемый при импорте, например forRoot() или forRootAsync(), который возвращает провайдеры, собранные из опций.
AsyncLocalStorage
API Node, который проносит хранилище запроса через асинхронные вызовы без передачи его аргументом.
Backpressure
Сигнал медленного потребителя производителю встать на паузу, чтобы данные не буферизовались в памяти без ограничений.
Driver adapter
В Prisma 7 — пакет, который соединяет сгенерированный клиент с драйвером базы вроде pg и сам владеет пулом.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.