Полка сервера: что уже есть в Node и Nest, и что брать для схем, базы и очередей

пакеты≈ 19 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Снимок: сентябрь 2026, Node 26.5.0 и Nest 12. Часть VI собрана руками: свой цикл событий, свой разборщик HTTP, свой контейнер, свой конвейер, своя таблица с индексами, свой раннер миграций. У каждой миниатюры есть стена — место, где дальше не дописывают, а берут чужое. Полка — список этих мест: на каждой задаче сказано, что уже умеет платформа, где в учебнике своя сборка, какой пакет брать и чем он за это берёт. Цена пакета на сервере считается не в килобайтах: в браузер он не едет, зато тянет чужие зависимости, просит своё хранилище или держит весь проект на старом мажоре каркаса. Версии здесь — снимок на дату проверки, а не вечная истина; как проверить строку самому, разбирают «Живой ли пакет» и свой аудитор зависимостей.

Разделы темы:·

Полка устроена так же, как вся часть: сначала то, что дают Node и Nest, и только за стеной — пакет. У каждой строки таблицы ниже есть паспорт: когда пакет не нужен, что умеет платформа, где в учебнике своя сборка, снимок с датой проверки и условие, при котором строку пора пересмотреть.

ЗадачаСначалаЕсли мало
схемы и конфигJSON Schema самого Fastify; @nestjs/config со Standard Schemazod, а где важен вес клиента — valibot
доступ к базеpg с параметрами; node:sqlite для скриптовDrizzle поверх того же pg
очередитаблица-журнал с ключом идемпотентностиpg-boss на своей базе; BullMQ, если хранилище не одно
расписаниеsetInterval, пока процесс один@nestjs/schedule
логиConsoleLogger с jsonpino и nestjs-pino
тестыnode:test и app.injectVitest; для базы — testcontainers
документация APIничего, если API ест только свой клиент@nestjs/swagger, для tRPC — trpc-to-openapi
токены и паролиnode:crypto: argon2, scrypt, crypto.subtlejose и @node-rs/argon2
защита HTTPзаголовки и лимит на обратном прокси@fastify/helmet и @nestjs/throttler

У полки браузера отметка «что умеет платформа» — это Baseline, поддержка во всех основных браузерах. Здесь мерило другое: платформа — установленный у вас Node и Nest, а у возможностей Node есть собственный статус стабильности в его документации. Следующий раздел спрашивает об этом сам Node, запуском.

В колонке «Вес или цена» у серверной полки стоит цена, а не килобайты. Серверный код в браузер не едет, и весом после сборщика тут никого не напугать; платит проект другим — числом чужих зависимостей, нужным хранилищем, нативным бинарником под платформу и строкой peerDependencies, которая решает, какой мажор каркаса вам разрешён.

Девять задач — не весь список серверных пакетов учебника: разборщик составного тела с пределом на размер разобран своей статьёй, и там же одной строкой назван sharp — тот самый, который в браузерной ветке приезжает вместе с Next. Своей строки на полке у него нет: пакета не было в носителе прогонов, а без снятого снимка версии паспорт не пишут. На этой полке то, что стоит вплотную к каркасу, базе и транспорту.

Строка полки стареет не по календарю, а по событию: вышел новый мажор, пакет пометили deprecated, peer наконец пустил свежий Nest. Поэтому у каждого паспорта своё «Пересмотреть, если…».

Вопрос 0 метода — «умеет ли это язык или платформа сам» — на сервере сильнее, чем в браузере. Браузер у читателя чужой, и о нём приходится спрашивать таблицы поддержки. Node — ваш: он лежит в образе, его версию выбираете вы, и на вопрос 0 он отвечает сам. За последние мажоры он ответил на него так, что с полки ушли четыре пакета разом.

прогон node 26.5.0 · 29 сентября 2026
Шесть возможностей платформы, каждая — запуском, а не по документации
// Вопрос 0 полки задан самой платформе: не «что пишут в статьях», а что отвечает
// установленный node. Каждая строка вывода — отдельная проверка, по порядку.
import { argon2Sync } from 'node:crypto';
import { DatabaseSync } from 'node:sqlite';

const строки: string[] = [];
const отметить = (что: string, чем: string) => строки.push(`${что.padEnd(16)} ${чем}`);

отметить('стирание типов', `process.features.typescript = ${process.features.typescript}`);
отметить('--env-file', `SHELF_NOTE = ${process.env.SHELF_NOTE}`);
отметить('fetch', `typeof fetch = ${typeof fetch}`);

const { describe, it, test } = await import('node:test');
отметить('node:test', `describe ${typeof describe}, it ${typeof it}, test ${typeof test}`);

const база = new DatabaseSync(':memory:');
база.exec('create table полка (имя text primary key)');
база.prepare('insert into полка (имя) values (?)').run('zod');
const { сколько } = база.prepare('select count(*) as сколько from полка').get() as { сколько: number };
отметить('node:sqlite', `DatabaseSync(':memory:'), строк после вставки: ${сколько}`);
база.close();

// соль постоянная: у прогона не должно быть нового вывода при каждом запуске
const ключ = argon2Sync('argon2id', {
  message: 'пароль',
  nonce: Buffer.alloc(16, 7),
  parallelism: 1,
  tagLength: 32,
  memory: 19456,
  passes: 2,
});
отметить('crypto.argon2', `argon2Sync вернул ${ключ.constructor.name}, длина ${ключ.length}`);

console.log(строки.join('\n'));
node --env-file=.env builtins.ts

Чего в этом выводе нет — седьмой строки таблицы ниже и статуса стабильности. --watch прогоном не снять: он держит процесс живым и ждёт правки файла. Со статусом сложнее. Его печатать нечем: это не свойство объекта, а пометка в документации того же выпуска. Её и надо смотреть рядом, потому что «работает» и «не поменяется в минорной версии» — разные обещания.

ВозможностьСтатус в документации Node 26.5.0Что с ней ушло с полки
стирание типов, node файл.ts2 — Stable (стабильно с 25.2.0 и 24.12.0)ts-node
--watchстабилен с 22.0.0 и 20.13.0nodemon
--env-fileне экспериментальный с 24.10.0 и 22.21.0dotenv
fetchне экспериментальный с 21.0.0node-fetch
node:test2 — Stable—
node:sqlite1.2 — Release candidate—
crypto.argon2пометки у метода нет — действует Stable модуля—

Две строки читаются не так, как кажется. «Release candidate» у node:sqlite — не «сырое»: это предпоследняя ступень, на которой ломающих правок уже не ждут, но и не обещают; для скрипта и теста этого хватает, для боевого хранилища — решайте сами. А у crypto.argon2 пометки нет вовсе, и это значит стабильность модуля node:crypto целиком: метод появился в 24.7.0 с пометкой «1.2 — Release candidate», и она держалась ещё в 26.0.0, а в установленном 26.5.0 и в вышедшем позже него 24.21.0 её уже нет.

И одна честная оговорка к первой строке. Стирание типов не заменяет компилятор: node вычёркивает аннотации, но декораторы и parameter properties он не выполняет — код с ними он просто откажется запускать. Поэтому проект на Nest собирают tsc, и записанные прогоны настоящего Nest в этой части сняты командой nest start, а не node src/main.ts; что именно ломается без метаданных, показала глава о декораторах Nest.

Вопрос 0 задают дважды: когда выбирают пакет и когда обновляют Node. Второй раз о нём обычно забывают — и в проекте годами живёт зависимость, работу которой платформа взяла на себя.

Не нужна, если сервер — чистый Fastify: проверка тела там уже есть, JSON Schema. Своя сборка: разбор входа с выводом типа и переходник к общему договору — в «Собираем валидатор границы», а схема окружения в ConfigModule.forRoot — в статье о загрузке настроек.

Прогон ниже ставит рядом две двери в одно и то же приложение. У левой схема описана средствами самого Fastify — объектом schema.body, который он отдаёт своему встроенному проверяющему; пакетов ради этого не поставлено ни одного. У правой — схема из zod, разобранная через договор Standard Schema. Прогноз: обе двери получают пустой заголовок; совпадут ли коды ответа и совпадут ли тела отказа?

прогон node 26.5.0 · 29 сентября 2026
Одно тело, две проверки: встроенная в Fastify и библиотечная
// Два обработчика одного приложения. У левого схема описана средствами самого
// Fastify, у правого — схемой из пакета через договор «~standard»
import Fastify from 'fastify';
import { z } from 'zod';

type Замечание = { readonly message: string };
type Итог = { readonly issues?: readonly Замечание[] };
type Схема = { readonly '~standard': { readonly vendor: string; readonly validate: (value: unknown) => Итог | Promise<Итог> } };

const app = Fastify({ logger: false });

app.post(
  '/json-schema',
  {
    schema: {
      body: {
        type: 'object',
        required: ['title'],
        additionalProperties: false,
        properties: { title: { type: 'string', minLength: 1 } },
      },
    },
  },
  (request) => ({ принято: request.body }),
);

const заметка: Схема = z.object({ title: z.string().min(1) });

app.post('/standard', async (request, reply) => {
  const итог = await заметка['~standard'].validate(request.body);
  if (итог.issues) return reply.code(400).send({ vendor: заметка['~standard'].vendor, причины: итог.issues.map((з) => з.message) });
  return { принято: request.body };
});

for (const тело of [{ title: '' }, { title: 'полка' }]) {
  for (const дверь of ['/json-schema', '/standard']) {
    const ответ = await app.inject({ method: 'POST', url: дверь, payload: тело });
    console.log(`${дверь} ${JSON.stringify(тело)} → ${ответ.statusCode} ${ответ.body}`);
  }
}
await app.close();
node standard.ts

Коды совпали, тела — нет. Встроенный проверяющий Fastify отвечает своей формой отказа: statusCode, code с машинным именем FST_ERR_VALIDATION и человеческая строка с путём поля. Библиотека отдаёт причины списком, и что из них увидит клиент, решаете вы. Для одного сервиса, у которого схема нужна только на входе, встроенного хватает: язык описания — JSON Schema, он старше любой из библиотек и на сервере уже есть.

Библиотека нужна, когда схема должна жить не только на входе: из неё выводят тип, её везут в контракт к клиенту, по ней собирают OpenAPI. Тогда и встаёт выбор между zod и valibot, и он дешевле, чем кажется: обе объявляют свойство ~standard, и код, который читает схему через него, имени библиотеки не знает — это уже проверено прогоном в главе о контрактах. На сервере вес не аргумент, он становится аргументом, когда та же схема едет в браузер или в телефон.

И капкан, из-за которого схема может стоять в коде и не проверять ничего. В Nest 12 у @Body, @Query и @Param есть поле schema, но декоратор только прикладывает схему к метаданным параметра. Проверяет её StandardSchemaValidationPipe, а регистрирует трубу приложение — строкой app.useGlobalPipes. Строки нет — и сервер молча отвечает 201 на пустой заголовок; прогон с обоими вариантами стоит в статье о валидаторе.

Третьего кандидата на этой строке нет. class-transformer, на котором стояла проверка входа в Nest прежних версий, последний раз выпускался 22.11.2021; строить на нём границу нового сервера незачем — раздел «Сошли с полки» ниже.

БиблиотекаВерсияВес или ценаПроверено
zod4.6.50 зависимостей; один пакет и на сервере, и в контракте29.09.2026
valibot1.5.00 зависимостей; peer typescript >=529.09.2026

Пересмотреть, если у zod сменится мажор или Standard Schema перестанет быть общим знаменателем: вся выгода этой строки в том, что имя библиотеки не разъезжается по коду.

Не нужна для пары запросов: pg с параметрами умеет всё, что вам нужно. Своя сборка: таблица со строками, индексами и уникальностью — в «Собираем таблицу», описание настоящих таблиц типами — в «Схеме на TypeScript», а чего у модели нет и чем настоящая база от неё отличается — в «Настоящем Postgres».

Граница здесь проходит не по числу таблиц, а по числу мест, где один и тот же запрос собирают заново. Пока запросов десяток и все они в одном репозитории, pg с параметрами читается лучше любого построителя: видно ровно тот SQL, который уйдёт в базу. Когда та же выборка нужна в пяти местах с разными условиями, а колонки должны совпадать с типами в TypeScript, начинается ручная синхронизация — и вот на ней Drizzle и окупается.

Для скриптов и тестов на полке ничего нет вовсе: node:sqlite встроен, и разовому скрипту вокруг миграций или тесту, которому нужна любая база, этого достаточно. Статус у модуля — «Release candidate», и для боевого хранилища это повод подумать, а для скрипта — нет.

БиблиотекаВерсияВес или ценаПроверено
drizzle-orm0.45.30 зависимостей; 29 необязательных peer — драйверы, типы к ним и трассировка29.09.2026
pg8.23.06 зависимостей; peer pg-native необязательный29.09.2026

Обе строки живут парой: Drizzle — это типы и построитель поверх драйвера, а ходит в базу всё равно pg. Отсюда и цена: своих зависимостей у drizzle-orm нет ни одной, зато peerDependencies у него 29 — и все необязательные: драйверы, из которых вы ставите один, пакеты типов к ним и @opentelemetry/api для трассировки. Отдельно стоит знать про 1.0: под тегом rc в реестре лежит 1.0.0-rc.4 от 27.06.2026, а тег latest по-прежнему показывает на нулевой мажор.

Пересмотреть, если Drizzle 1.0 станет стабильной: нулевой мажор — единственная причина, по которой обновление этого пакета до сих пор читают как ломающее.

Не нужна, пока фоновой работы нет: у стека автора очереди нет вовсе. Своей сборки у этой строки тоже нет — и это не пробел: всё, из чего делается простая очередь, в части уже собрано. Строка в таблице с уникальным индексом, ключ идемпотентности, который не даёт выполнить заявку дважды, и колонка с отметкой — больше ничего не нужно, пока заданий немного.

Пример ниже ставит рядом два хранилища заданий с одним и тем же наружным договором: положить, перезапустить процесс, поработать. Слева — массив внутри процесса, справа — таблица главы 9 с уникальным индексом по ключу. Часы ручные: время двигает сам пример. Прогноз: сколько заданий будет ждать у каждой очереди после повторной заявки и что уйдёт после «перезапуска»?

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Массив теряет всё: перезапуск процесса — это и есть новый массив. И повторную заявку он принимает как новую, потому что отличать их нечем. У таблицы обе задачи решает один уникальный индекс: вторая заявка с тем же ключом отбивается кодом 23505, а строка переживает процесс, потому что живёт не в нём. Ровно этим приёмом устроен приём пачки в главе о синхронизации — память вердиктов по opId. А чего стоит обойтись без строки, видно там, где её не завели: пауза между письмами восстановления держится в памяти процесса, и перезапуск обнуляет её ровно так же, как массив в примере выше.

Пакет становится нужен, когда к «положить и выполнить» добавляются расписание, повторы с нарастающей паузой, приоритеты, блокировка задания за одним из нескольких работников и панель, где всё это видно. Писать это самому — уже не выходной проект. На полке два кандидата, и выбирают между ними не по удобству, а по хранилищу.

БиблиотекаВерсияВес или ценаПроверено
pg-boss12.35.04 зависимости, одна из них pg; только PostgreSQL29.09.2026
bullmq6.3.105 зависимостей; хранилище — Redis, Valkey или PostgreSQL29.09.2026

pg-boss кладёт задания в ту же базу PostgreSQL, что и данные: отдельного сервиса в compose не появляется, а резервная копия базы забирает очередь заодно. Цена — четыре зависимости, среди которых тот же pg, и привязка к одной базе. У BullMQ хранилище другое, и тут стоит прочитать peerDependencies внимательно: там четыре имени — redis, ioredis, pg и bullmq-otel, — и все четыре помечены необязательными. Значит, это не требования, а список того, с чем пакет умеет работать: первые три — клиенты хранилищ (у BullMQ 6 внутри лежат и команды для PostgreSQL), четвёртый — отправка телеметрии. Ставите то, на чём работаете.

Пересмотреть, если появится второй работник: тогда из таблицы-журнала придётся доставать задания так, чтобы двое не взяли одно и то же, — и вот здесь свой велосипед обычно и ломается.

Не нужен, пока процесс один: хватит setInterval при старте. Платформа здесь — таймеры Node, и фаза, в которой они срабатывают, разобрана в первой статье о цикле событий.

@nestjs/schedule добавляет к этому то, чего у голого таймера нет: декораторы @Cron, @Interval и @Timeout на методах провайдеров, разбор выражений cron, именованные задания, которые можно остановить и запустить, и остановку всех заданий вместе с приложением. Прямая зависимость у него одна, а peer он объявляет ровно на два мажора Nest — одиннадцатый и двенадцатый.

БиблиотекаВерсияВес или ценаПроверено
@nestjs/schedule12.0.21 зависимость; peer Nest ^11 || ^1229.09.2026

Ловушка у этой строки не в пакете, а в выкладке. Задание по расписанию живёт в процессе, и если процессов два — например, приложение подняли в двух репликах, — сработает оно дважды. Ни пакет, ни таймер об этом не знают: договариваться, кто сегодня главный, придётся самим — отметкой в базе, за которую работники спорят тем же уникальным индексом, что и в разделе про очереди. Поэтому строка расписания и стоит на полке рядом с очередью: у pg-boss расписание своё, и оно про это уже знает.

Пересмотреть, если приложение перестанет быть одним процессом. Признак, что пора: задание начало приходить дважды, а в журнале это видно только по двойной записи.

Не нужен для одного сервиса: у ConsoleLogger в Nest есть json и structuredParams. Своя сборка и разбор четырёх наборов настроек логгера — в статье о логах и мягкой остановке; там же сказано, что файл, ротацию и отправку в хранилище приложение на себя не берёт.

БиблиотекаВерсияВес или ценаПроверено
pino10.3.111 зависимостей; вывод — JSON, читаемый вид даёт отдельный процесс29.09.2026
nestjs-pino5.2.1своих зависимостей нет; peer pino ^10, pino-http ^11, Nest ^11.0.8 || ^12.0.229.09.2026

Что даёт pino сверх встроенного: дочерние логгеры, у которых к каждой записи автоматически приписаны поля родителя (номер запроса, идентификатор пользователя), вычищение секретных полей по путям при создании логгера и скорость, ради которой он и написан. Цена — одиннадцать прямых зависимостей и вывод, который человеком не читается: красивый вид даёт отдельный процесс, через который вывод пропускают в разработке.

nestjs-pino — переходник, а не второй логгер: своих зависимостей у него нет, зато в peer он держит pino, pino-http, rxjs и два пакета Nest. Это и есть его цена: пять чужих версий, которые должны сойтись, чтобы обновить один из них.

Пересмотреть, если peer nestjs-pino отстанет от мажора Nest: переходник — самое хрупкое звено, потому что версий, которые должны сойтись, у него больше всех.

Не нужен для чистых функций и скриптов: node --test уже стоит. Своя сборка раннера — describe, it, ожидания и асинхронность — в «Собираем свой тест-раннер»; там же сравнение встроенного раннера с Vitest и jest по тому, что они добавляют сверх скелета.

Проекту на Nest раннер достаётся со стартером, и выбирает его один вопрос при создании: nest new спрашивает систему модулей и по умолчанию предлагает ESM, а к ней — Vitest. В шаблоне ESM у @nestjs/schematics 12.0.2 стоят vitest ^4.1.2, @vitest/coverage-v8 ^4.1.2, supertest ^7.0.0 и oxlint; во втором шаблоне, CommonJS, на том же месте jest ^30.0.0 с ts-jest. Значит, из стартера jest не ушёл — он остался на ветке, которую выбирают вторым пунктом. В боевом API учебника при этом живёт jest 29 — ровно тот случай, когда полка описывает новый проект, а не переписывает старый.

БиблиотекаВерсияВес или ценаПроверено
vitest5.0.213 зависимостей; peer vite ^6.4.0 || ^7.0.0 || ^8.0.029.09.2026
jest30.5.24 зависимости; стартер Nest 12 ставит его только на ветке CommonJS29.09.2026
supertest7.3.03 зависимости; не нужен, пока хватает app.inject29.09.2026
testcontainers12.2.015 зависимостей; нужен работающий docker29.09.2026

Растяжка у строки Vitest стоит не на нём самом, а на vite: мажор сборщика решает, увидит ли Nest метаданные декораторов в тестах. Проверено 14.09.2026 на одной и той же папке: vitest 4.1.11 с vite 8.3.0 собирает Test.createTestingModule и внедряет зависимости по типу без единого плагина, а та же папка с vite 7.3.6 даёт design:paramtypes равный undefined и падает TypeError. Диапазон peer у Vitest пускает оба, так что «поставилось и работает» ничего не гарантирует: закрепляйте vite восьмой.

Две остальные строки — про то, чего нет у самого раннера. HTTP в Nest тестируют без сети: app.inject отдаёт ответ, не открывая порта, и supertest нужен там, где запрос должен пройти через настоящий сокет. А базу в тестах подменяют не заглушкой, а базой: testcontainers поднимает настоящий PostgreSQL в контейнере на время прогона — о том, где подмена врёт, а настоящая база нет, есть отдельная статья. Цена у этой строки самая высокая на полке: пятнадцать зависимостей и работающий docker на машине, где идут тесты.

Пересмотреть, если стартер Nest сменит раннер ещё раз. Эта строка меняется не тогда, когда выходит новая версия Vitest, а тогда, когда меняется шаблон, с которого начинают проект.

Не нужна, если API ест только свой клиент на TypeScript. Типы у такого клиента выводятся из роутера, и промежуточный документ никто не читает. Своя сборка — описание из той же таблицы метаданных, которую собирает каркас, — в статье об OpenAPI.

БиблиотекаВерсияВес или ценаПроверено
@nestjs/swagger12.0.27 зависимостей; peer @nestjs/core ^12.0.029.09.2026
trpc-to-openapi3.3.03 зависимости; peer zod-openapi ^5.4.429.09.2026

@nestjs/swagger собирает документ по декораторам контроллеров, и его peer прибит к своему мажору Nest намертво: ^12.0.0, без вариантов. Для процедур tRPC он бесполезен — плагин на транспорте контроллеров не видит, — и там нужен второй пакет, trpc-to-openapi. Тот, в свою очередь, держит в peer zod-openapi: документ собирается из схем, и схемы должны быть zod.

Имя trpc-openapi, без to посередине, — это другой пакет и тупик: его последний выпуск от 24.05.2023, а peer требует @trpc/server ^10.0.0. На tRPC 11 он не встанет, и искать его в реестре по названию — обычная ошибка.

Пересмотреть, если API начнут читать чужие: документ нужен не тогда, когда его удобно собрать, а тогда, когда у него появляется читатель вне вашей команды.

Не нужны: argon2, scrypt и crypto.subtle встроены в node:crypto. Своя сборка: стоимость хеша, соль и проверка пароля — в статье «Пароли: почему хеш, соль и стоимость», а разбор и подпись JWT руками — в «Собираем токен».

Разница между встроенным и пакетом здесь тоньше, чем в остальных строках, и стоит её назвать. crypto.argon2Sync возвращает Buffer — сырой ключ длиной, которую вы заказали. Ни соли, ни параметров, ни знакомой строки вида $argon2id$v=19$m=… в нём нет: формат хранения придумываете вы, и вы же следите, чтобы при смене стоимости старые хеши продолжали проверяться. Библиотеки argon2 отдают готовую строку со всеми параметрами внутри — за это их и берут. У scrypt звёздочка своя: минимум OWASP N = 217 и r = 8 просит около 128 МиБ, а maxmem по умолчанию — 32 МиБ, и вызов падает «memory limit exceeded», не начав считать. Память тут и есть стоимость, и разрешают её явно.

БиблиотекаВерсияВес или ценаПроверено
jose6.2.120 зависимостей; чистый JavaScript, без бинарника29.09.2026
@node-rs/argon22.2.1прямых зависимостей нет; 13 бинарников по платформам в optionalDependencies29.09.2026

jose — для JWT и всего вокруг него: проверка подписи чужим ключом, наборы ключей по адресу JWKS, шифрованные токены. Зависимостей у него нет ни одной, и он чистый JavaScript, то есть одинаково едет и в образ, и в браузер. @node-rs/argon2 — противоположный случай: прямых зависимостей у него тоже нет, зато в optionalDependencies лежат тринадцать бинарников по платформам, и npm поставит тот, что подойдёт машине. Этим вы и платите за скорость проверки пароля — и за то, что образ придётся собирать под ту же платформу, на которой он поедет.

В боевом API учебника при этом стоят jsonwebtoken 9.0.3 и bcryptjs 3.0.3, и менять их не на что: оба живы, оба делают свою работу. Полка описывает выбор для нового проекта, а не список того, что пора переписать.

Пересмотреть, если появится второй сервис, которому нужно проверять ваши токены: ручная проверка подписи перестаёт быть дешевле готовой библиотеки ровно в этот момент.

Не нужны, если заголовки и лимит стоят на обратном прокси. Своя сборка: ограничитель частоты по паре «процедура и адрес» — в статье о прослойках tRPC, а сам прокси с сертификатом и заголовками — в главе о выкладке.

БиблиотекаВерсияВес или ценаПроверено
@fastify/helmet13.1.12 зависимости; не нужен, если заголовки ставит Caddy29.09.2026
@nestjs/throttler6.7.10 зависимостей; счётчик живёт в памяти одной реплики29.09.2026

@fastify/helmet ставит набор заголовков безопасности одной строкой. Смысл он имеет там, где перед приложением ничего нет; если наружу смотрит Caddy и заголовки прописаны в его конфиге, второй набор от приложения только добавит места, где они могут разойтись.

У @nestjs/throttler зависимостей нет вовсе, а счёт он ведёт в памяти процесса. Пока процесс один, это честный ограничитель; как только реплик становится две, лимит в десять вызовов превращается в двадцать. Отдельно стоит запомнить историю его peer: ещё в версии 6.5.0 диапазон кончался на ^11.0.0, и пакет нельзя было поставить к Nest 12 без насилия над разрешением зависимостей. В 6.6.0 от 16.09.2026 двенадцатый мажор пустили, и на 29.09.2026 версия 6.7.1 объявляет весь ряд с седьмого по двенадцатый. Строка полки, написанная двумя неделями раньше, была бы верной и устаревшей одновременно — за этим и нужна дата проверки.

Пересмотреть, если приложение переедет за прокси, который умеет лимит сам: тогда ограничитель в приложении остаётся только для того, что прокси не различает, — для конкретной процедуры и конкретного пользователя.

Полка — не список хороших пакетов, а список решений с датой. Шесть строк ниже когда-то стояли бы в таблицах выше; сегодня их место занято, и знать об этом полезнее, чем о новинках: именно эти имена чаще всего переносят в новый проект по привычке.

ПакетЧто взяло его работуПроверено
ts-node 10.9.2стирание типов в самом node; выпуска нет с 08.12.202329.09.2026
nodemon 3.1.14node --watch29.09.2026
dotenv 18.0.4node --env-file29.09.2026
node-fetch 3.3.2fetch в node; выпуска нет с 25.07.202329.09.2026
trpc-openapi 1.2.0trpc-to-openapi: peer старого держит tRPC 10; выпуска нет с 24.05.202329.09.2026
class-transformer 0.5.1схема на входе вместо декораторов поля; выпуска нет с 22.11.202129.09.2026

Две строки требуют оговорки, иначе они читаются как обещание, которого полка не даёт. dotenv ушёл из ваших зависимостей, но не из проекта: @nestjs/config тянет его сам — в 12.0.0 это 17.4.2, а в 12.0.1 от 22.09.2026 уже 18.0.3. И ts-node ушёл не потому, что node научился всему TypeScript, а потому, что node научился стирать типы: проект с декораторами всё равно собирают компилятором.

Обратите внимание, у каких строк в средней колонке стоит дата последнего выпуска, а у каких нет: nodemon и dotenv живы и выпускаются, а сошли с полки не по здоровью. Вопрос 1 — «жив ли пакет» — они проходят; их отсеял вопрос 0, и это единственный вопрос метода, ответ на который меняется не у пакета, а у вас: он зависит от того, какой Node стоит в образе.

Список «сошли с полки» ведут ради обновлений: когда в проекте поднимают мажор Node, первым делом смотрят сюда — не осталось ли в зависимостях того, что новая платформа делает сама.

Пять имён ниже встречаются в серверных проектах постоянно, и ни одно не попало в таблицы выше. Причина у каждого своя, и ни у одного она не «плохой пакет».

ПакетПочему не на полкеПроверено
Prisma, prismaвопрос 4: свой язык схемы и шаг генерации клиента. Плюс ловушка тега: latest указывает на 8.0.0-rc.19, то есть на предварительную версию29.09.2026
TypeORM, typeorm 1.1.1вопрос 4: схема описывается классами-сущностями и декораторами поля — тем же способом, от которого ушла проверка входа29.09.2026
Express, express 5.2.1не выбор по умолчанию для нового Nest: адаптер Fastify в части VI берут ради скорости и схем, а у самого Express 28 прямых зависимостей29.09.2026
nestjs-trpc 2.13.0peer держит Nest ^9.3.8 || ^10 || ^11: к Nest 12 не встаёт29.09.2026
nestjs-zod 5.5.0peer держит Nest ^10 || ^11: к Nest 12 не встаёт29.09.2026

Первые три строки — про устройство проекта, и спорить с ними можно: выбор между Express и Fastify эта часть намеренно не делает за вас, а называет его цену. Последние две — про другое, и это главная серверная опасность, которой нет у полки браузера.

Пакет-надстройка над каркасом объявляет в peerDependencies, с какими мажорами каркаса он согласен работать. Пока эта строка не расширена, весь проект стоит на старом мажоре — не потому, что новый плох, а потому, что одна надстройка не успела. Именно поэтому часть VI монтирует tRPC штатным адаптером @trpc/server на экземпляр FastifyAdapter, а не через nestjs-trpc, и разбирает вход схемой прямо в процедуре, а не через nestjs-zod. И поэтому же @nestjs/throttler из раздела о защите HTTP стоит на полке, а эти два — нет: у него строку расширили, у них пока нет. А боевой API учебника как раз на nestjs-trpc и стоит — и держится из-за него на Nest 11. Это не двойные стандарты: полка описывает выбор для нового проекта, а рабочий код переезжает тогда, когда переезд дешевле, чем ожидание.

На этом полка сервера закончена, а с ней и часть VI. Таких полок в учебнике четыре: браузера, React и Next, React Native и Expo и эта. Мерило платформы у всех своё — Baseline, React и Next, React Native с модулями SDK, статус в документации Node, — и цена считается по-разному: килобайтами после сборщика, клиентом Expo, чужими зависимостями. А порядок вопросов один и тот же, и начинается он не с выбора пакета, а с вопроса, нужен ли он.

Строка «не на полке» — такое же решение с датой, как строка полки. У пакета, который не взяли из-за peer, дата означает ровно одно: когда в последний раз смотрели, не расширили ли диапазон.

  • Цена пакета на сервере — не килобайты: в браузер он не едет. Платят числом чужих зависимостей, нужным сервисом рядом, нативным бинарником и строкой peerDependencies, которая решает, какой мажор каркаса вам разрешён.
  • Вопрос 0 на сервере задают самому Node, запуском: process.features.typescript, --env-file, fetch, node:test, node:sqlite и crypto.argon2 отвечают за себя сами. Статус стабильности рядом с ними приходится смотреть в документации того же выпуска — свойства с ним нет.
  • Четыре имени ушли с полки не по здоровью: ts-node, nodemon, dotenv и node-fetch отсеял вопрос 0, а не вопрос 1. Ответ на него меняется не у пакета, а у вас — вместе с мажором Node в образе.
  • Библиотека схем не нужна там, где схема живёт только на входе: у Fastify есть JSON Schema. Она нужна, когда из схемы выводят тип, везут её в контракт и собирают по ней документ; выбор между zod и valibot дешёв, пока код вокруг читает их через ~standard.
  • Очередь начинается не с пакета: строка в таблице с уникальным индексом по ключу идемпотентности переживает перезапуск и не даёт выполнить заявку дважды. pg-boss и BullMQ берут за повторы, расписание, приоритеты и панель — и за выбор хранилища.
  • Надстройка над каркасом держит весь проект на своём мажоре: nestjs-trpc и nestjs-zod на 29.09.2026 не пускают Nest 12, а @nestjs/throttler пустил его в 6.6.0 — за две недели до этой проверки. Поэтому у каждой строки полки стоит дата, а не только версия.

1/4В контроллере Nest 12 стоит @Body({ schema }) со схемой zod, а сервер отвечает 201 на пустой заголовок. Где причина?

2/4У BullMQ 6 в peerDependencies стоит pg версии >=8.0.0. Что это значит?

3/4Полка браузера пишет в колонке «Вес или цена» килобайты после сборщика. Почему у полки сервера в той же колонке другое?

4/4Что на 29.09.2026 печатает process.features.typescript в Node 26.5.0 — и что из этого следует для проекта на Nest?

Растяжка, которая молчит

В книге учёта у каждой строки записаны растяжки — условия, при которых строка перестаёт быть правдой. Здесь их три вида, а считается пока один: npm-major. Допишите сработала: npm-stale — выпуск latest старше, чем months календарных месяцев (день выпуска плюс месяцы раньше today); npm-peer — строка peerDependencies[peer] у latest не равна записанной was. Прогноз до правки: сколько строк из четырёх скажут что-то, кроме «тихо»?

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Календарные месяцы считает сам Date.UTC: месяц с переполнением он переносит на следующий год.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Сработали две растяжки из пяти. У @nestjs/throttler в диапазон добавился ^12.0.0 — строку полки надо переписать, пакет теперь годится для Nest 12. У trpc-openapi выпуска нет с 24.05.2023, и это ровно та причина, по которой он в разделе «Сошли с полки». А pino и drizzle-orm промолчали правильно: мажор у обоих тот же, а последнему выпуску pino на эту дату чуть больше семи месяцев, а не двенадцать.

Две детали, из-за которых такая проверка врёт чаще всего. Даты сравнивают по календарному дню UTC: метка реестра — это момент времени, и 2026-09-21T10:06:39Z строго больше, чем 2026-09-21, так что растяжка «выпуск позже такой-то даты» срабатывала бы в день записи. А строку диапазона сравнивают как строку, а не разбирают: ^11.0.0 || ^12.0.0 и ^12.0.0 || ^11.0.0 для проверки разные, и это правильно — человек должен посмотреть сам.

Письмо, которое не ушло

Очередь из раздела о фоновой работе выросла до отправки писем: work берёт готовые задания и зовёт send. Почтовый сервер отвечает отказом на первое письмо Боре и соглашается на второе — так бывает всегда. Прогноз: сколько писем окажется в ящике после двух проходов и сколько заданий останется ждать? Потом переставьте одну строку так, чтобы отказ не считался выполнением.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Отметка стоит до отправки, поэтому задание считается выполненным даже тогда, когда письмо не ушло. Достаточно поменять местами две строки — и добавить send в try, чтобы отказ до отметки не доходил:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Теперь первый проход кладёт в ящик одно письмо и оставляет одно задание ждать, а второй забирает и его: в ящике два, ждущих нет. Ровно этот порядок — «сделать, потом отметить» — и отличает журнал заданий от списка намерений.

Тем же одним шагом появляется и новая беда, ради которой берут пакет. Если процесс умрёт между send и update, письмо уйдёт дважды: отметки-то нет. Полностью это не лечится ничем — доставка «ровно один раз» через границу процесса невозможна, — но pg-boss и BullMQ дают то, что здесь пришлось бы писать самому: счётчик попыток, нарастающую паузу перед повтором и предел, после которого задание уходит в отдельный список, а не крутится вечно.