Полка сервера: что уже есть в Node и Nest, и что брать для схем, базы и очередей
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Снимок: сентябрь 2026, Node 26.5.0 и Nest 12. Часть VI собрана руками: свой цикл событий, свой разборщик HTTP, свой контейнер, свой конвейер, своя таблица с индексами, свой раннер миграций. У каждой миниатюры есть стена — место, где дальше не дописывают, а берут чужое. Полка — список этих мест: на каждой задаче сказано, что уже умеет платформа, где в учебнике своя сборка, какой пакет брать и чем он за это берёт. Цена пакета на сервере считается не в килобайтах: в браузер он не едет, зато тянет чужие зависимости, просит своё хранилище или держит весь проект на старом мажоре каркаса. Версии здесь — снимок на дату проверки, а не вечная истина; как проверить строку самому, разбирают «Живой ли пакет» и свой аудитор зависимостей.
Полка устроена так же, как вся часть: сначала то, что дают Node и Nest, и только за стеной — пакет. У каждой строки таблицы ниже есть паспорт: когда пакет не нужен, что умеет платформа, где в учебнике своя сборка, снимок с датой проверки и условие, при котором строку пора пересмотреть.
| Задача | Сначала | Если мало |
|---|---|---|
| схемы и конфиг | JSON Schema самого Fastify; @nestjs/config со Standard Schema | zod, а где важен вес клиента — valibot |
| доступ к базе | pg с параметрами; node:sqlite для скриптов | Drizzle поверх того же pg |
| очереди | таблица-журнал с ключом идемпотентности | pg-boss на своей базе; BullMQ, если хранилище не одно |
| расписание | setInterval, пока процесс один | @nestjs/schedule |
| логи | ConsoleLogger с json | pino и nestjs-pino |
| тесты | node:test и app.inject | Vitest; для базы — testcontainers |
| документация API | ничего, если API ест только свой клиент | @nestjs/swagger, для tRPC — trpc-to-openapi |
| токены и пароли | node:crypto: argon2, scrypt, crypto.subtle | jose и @node-rs/argon2 |
| защита HTTP | заголовки и лимит на обратном прокси | @fastify/helmet и @nestjs/throttler |
У полки браузера отметка «что умеет платформа» — это Baseline, поддержка во всех основных браузерах. Здесь мерило другое: платформа — установленный у вас Node и Nest, а у возможностей Node есть собственный статус стабильности в его документации. Следующий раздел спрашивает об этом сам Node, запуском.
В колонке «Вес или цена» у серверной полки стоит цена, а не килобайты. Серверный код в браузер не едет, и весом после сборщика тут никого не напугать; платит проект другим — числом чужих зависимостей, нужным хранилищем, нативным бинарником под платформу и строкой peerDependencies, которая решает, какой мажор каркаса вам разрешён.
Девять задач — не весь список серверных пакетов учебника: разборщик составного тела с пределом на размер разобран своей статьёй, и там же одной строкой назван sharp — тот самый, который в браузерной ветке приезжает вместе с Next. Своей строки на полке у него нет: пакета не было в носителе прогонов, а без снятого снимка версии паспорт не пишут. На этой полке то, что стоит вплотную к каркасу, базе и транспорту.
Строка полки стареет не по календарю, а по событию: вышел новый мажор, пакет пометили deprecated, peer наконец пустил свежий Nest. Поэтому у каждого паспорта своё «Пересмотреть, если…».
Вопрос 0 метода — «умеет ли это язык или платформа сам» — на сервере сильнее, чем в браузере. Браузер у читателя чужой, и о нём приходится спрашивать таблицы поддержки. Node — ваш: он лежит в образе, его версию выбираете вы, и на вопрос 0 он отвечает сам. За последние мажоры он ответил на него так, что с полки ушли четыре пакета разом.
// Вопрос 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'));
стирание типов process.features.typescript = strip
--env-file SHELF_NOTE = строка из файла, а не из окружения
fetch typeof fetch = function
node:test describe function, it function, test function
node:sqlite DatabaseSync(':memory:'), строк после вставки: 1
crypto.argon2 argon2Sync вернул Buffer, длина 32Чего в этом выводе нет — седьмой строки таблицы ниже и статуса стабильности. --watch прогоном не снять: он держит процесс живым и ждёт правки файла. Со статусом сложнее. Его печатать нечем: это не свойство объекта, а пометка в документации того же выпуска. Её и надо смотреть рядом, потому что «работает» и «не поменяется в минорной версии» — разные обещания.
| Возможность | Статус в документации Node 26.5.0 | Что с ней ушло с полки |
|---|---|---|
стирание типов, node файл.ts | 2 — Stable (стабильно с 25.2.0 и 24.12.0) | ts-node |
--watch | стабилен с 22.0.0 и 20.13.0 | nodemon |
--env-file | не экспериментальный с 24.10.0 и 22.21.0 | dotenv |
fetch | не экспериментальный с 21.0.0 | node-fetch |
node:test | 2 — Stable | — |
node:sqlite | 1.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. Прогноз: обе двери получают пустой заголовок; совпадут ли коды ответа и совпадут ли тела отказа?
// Два обработчика одного приложения. У левого схема описана средствами самого // 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();
/json-schema {"title":""} → 400 {"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body/title must NOT have fewer than 1 characters"}
/standard {"title":""} → 400 {"vendor":"zod","причины":["Too small: expected string to have >=1 characters"]}
/json-schema {"title":"полка"} → 200 {"принято":{"title":"полка"}}
/standard {"title":"полка"} → 200 {"принято":{"title":"полка"}}Коды совпали, тела — нет. Встроенный проверяющий 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; строить на нём границу нового сервера незачем — раздел «Сошли с полки» ниже.
| Библиотека | Версия | Вес или цена | Проверено |
|---|---|---|---|
zod | 4.6.5 | 0 зависимостей; один пакет и на сервере, и в контракте | 29.09.2026 |
valibot | 1.5.0 | 0 зависимостей; peer typescript >=5 | 29.09.2026 |
Пересмотреть, если у zod сменится мажор или Standard Schema перестанет быть общим знаменателем: вся выгода этой строки в том, что имя библиотеки не разъезжается по коду.
Не нужна для пары запросов: pg с параметрами умеет всё, что вам нужно. Своя сборка: таблица со строками, индексами и уникальностью — в «Собираем таблицу», описание настоящих таблиц типами — в «Схеме на TypeScript», а чего у модели нет и чем настоящая база от неё отличается — в «Настоящем Postgres».
Граница здесь проходит не по числу таблиц, а по числу мест, где один и тот же запрос собирают заново. Пока запросов десяток и все они в одном репозитории, pg с параметрами читается лучше любого построителя: видно ровно тот SQL, который уйдёт в базу. Когда та же выборка нужна в пяти местах с разными условиями, а колонки должны совпадать с типами в TypeScript, начинается ручная синхронизация — и вот на ней Drizzle и окупается.
Для скриптов и тестов на полке ничего нет вовсе: node:sqlite встроен, и разовому скрипту вокруг миграций или тесту, которому нужна любая база, этого достаточно. Статус у модуля — «Release candidate», и для боевого хранилища это повод подумать, а для скрипта — нет.
| Библиотека | Версия | Вес или цена | Проверено |
|---|---|---|---|
drizzle-orm | 0.45.3 | 0 зависимостей; 29 необязательных peer — драйверы, типы к ним и трассировка | 29.09.2026 |
pg | 8.23.0 | 6 зависимостей; 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 с уникальным индексом по ключу. Часы ручные: время двигает сам пример. Прогноз: сколько заданий будет ждать у каждой очереди после повторной заявки и что уйдёт после «перезапуска»?
Массив теряет всё: перезапуск процесса — это и есть новый массив. И повторную заявку он принимает как новую, потому что отличать их нечем. У таблицы обе задачи решает один уникальный индекс: вторая заявка с тем же ключом отбивается кодом 23505, а строка переживает процесс, потому что живёт не в нём. Ровно этим приёмом устроен приём пачки в главе о синхронизации — память вердиктов по opId. А чего стоит обойтись без строки, видно там, где её не завели: пауза между письмами восстановления держится в памяти процесса, и перезапуск обнуляет её ровно так же, как массив в примере выше.
Пакет становится нужен, когда к «положить и выполнить» добавляются расписание, повторы с нарастающей паузой, приоритеты, блокировка задания за одним из нескольких работников и панель, где всё это видно. Писать это самому — уже не выходной проект. На полке два кандидата, и выбирают между ними не по удобству, а по хранилищу.
| Библиотека | Версия | Вес или цена | Проверено |
|---|---|---|---|
pg-boss | 12.35.0 | 4 зависимости, одна из них pg; только PostgreSQL | 29.09.2026 |
bullmq | 6.3.10 | 5 зависимостей; хранилище — Redis, Valkey или PostgreSQL | 29.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/schedule | 12.0.2 | 1 зависимость; peer Nest ^11 || ^12 | 29.09.2026 |
Ловушка у этой строки не в пакете, а в выкладке. Задание по расписанию живёт в процессе, и если процессов два — например, приложение подняли в двух репликах, — сработает оно дважды. Ни пакет, ни таймер об этом не знают: договариваться, кто сегодня главный, придётся самим — отметкой в базе, за которую работники спорят тем же уникальным индексом, что и в разделе про очереди. Поэтому строка расписания и стоит на полке рядом с очередью: у pg-boss расписание своё, и оно про это уже знает.
Пересмотреть, если приложение перестанет быть одним процессом. Признак, что пора: задание начало приходить дважды, а в журнале это видно только по двойной записи.
Не нужен для одного сервиса: у ConsoleLogger в Nest есть json и structuredParams. Своя сборка и разбор четырёх наборов настроек логгера — в статье о логах и мягкой остановке; там же сказано, что файл, ротацию и отправку в хранилище приложение на себя не берёт.
| Библиотека | Версия | Вес или цена | Проверено |
|---|---|---|---|
pino | 10.3.1 | 11 зависимостей; вывод — JSON, читаемый вид даёт отдельный процесс | 29.09.2026 |
nestjs-pino | 5.2.1 | своих зависимостей нет; peer pino ^10, pino-http ^11, Nest ^11.0.8 || ^12.0.2 | 29.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 — ровно тот случай, когда полка описывает новый проект, а не переписывает старый.
| Библиотека | Версия | Вес или цена | Проверено |
|---|---|---|---|
vitest | 5.0.2 | 13 зависимостей; peer vite ^6.4.0 || ^7.0.0 || ^8.0.0 | 29.09.2026 |
jest | 30.5.2 | 4 зависимости; стартер Nest 12 ставит его только на ветке CommonJS | 29.09.2026 |
supertest | 7.3.0 | 3 зависимости; не нужен, пока хватает app.inject | 29.09.2026 |
testcontainers | 12.2.0 | 15 зависимостей; нужен работающий docker | 29.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/swagger | 12.0.2 | 7 зависимостей; peer @nestjs/core ^12.0.0 | 29.09.2026 |
trpc-to-openapi | 3.3.0 | 3 зависимости; peer zod-openapi ^5.4.4 | 29.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», не начав считать. Память тут и есть стоимость, и разрешают её явно.
| Библиотека | Версия | Вес или цена | Проверено |
|---|---|---|---|
jose | 6.2.12 | 0 зависимостей; чистый JavaScript, без бинарника | 29.09.2026 |
@node-rs/argon2 | 2.2.1 | прямых зависимостей нет; 13 бинарников по платформам в optionalDependencies | 29.09.2026 |
jose — для JWT и всего вокруг него: проверка подписи чужим ключом, наборы ключей по адресу JWKS, шифрованные токены. Зависимостей у него нет ни одной, и он чистый JavaScript, то есть одинаково едет и в образ, и в браузер. @node-rs/argon2 — противоположный случай: прямых зависимостей у него тоже нет, зато в optionalDependencies лежат тринадцать бинарников по платформам, и npm поставит тот, что подойдёт машине. Этим вы и платите за скорость проверки пароля — и за то, что образ придётся собирать под ту же платформу, на которой он поедет.
В боевом API учебника при этом стоят jsonwebtoken 9.0.3 и bcryptjs 3.0.3, и менять их не на что: оба живы, оба делают свою работу. Полка описывает выбор для нового проекта, а не список того, что пора переписать.
Пересмотреть, если появится второй сервис, которому нужно проверять ваши токены: ручная проверка подписи перестаёт быть дешевле готовой библиотеки ровно в этот момент.
Не нужны, если заголовки и лимит стоят на обратном прокси. Своя сборка: ограничитель частоты по паре «процедура и адрес» — в статье о прослойках tRPC, а сам прокси с сертификатом и заголовками — в главе о выкладке.
| Библиотека | Версия | Вес или цена | Проверено |
|---|---|---|---|
@fastify/helmet | 13.1.1 | 2 зависимости; не нужен, если заголовки ставит Caddy | 29.09.2026 |
@nestjs/throttler | 6.7.1 | 0 зависимостей; счётчик живёт в памяти одной реплики | 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.2023 | 29.09.2026 |
nodemon 3.1.14 | node --watch | 29.09.2026 |
dotenv 18.0.4 | node --env-file | 29.09.2026 |
node-fetch 3.3.2 | fetch в node; выпуска нет с 25.07.2023 | 29.09.2026 |
trpc-openapi 1.2.0 | trpc-to-openapi: peer старого держит tRPC 10; выпуска нет с 24.05.2023 | 29.09.2026 |
class-transformer 0.5.1 | схема на входе вместо декораторов поля; выпуска нет с 22.11.2021 | 29.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.0 | peer держит Nest ^9.3.8 || ^10 || ^11: к Nest 12 не встаёт | 29.09.2026 |
nestjs-zod 5.5.0 | peer держит 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. Прогноз до правки: сколько строк из четырёх скажут что-то, кроме «тихо»?
Календарные месяцы считает сам Date.UTC: месяц с переполнением он переносит на следующий год.
Сработали две растяжки из пяти. У @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. Почтовый сервер отвечает отказом на первое письмо Боре и соглашается на второе — так бывает всегда. Прогноз: сколько писем окажется в ящике после двух проходов и сколько заданий останется ждать? Потом переставьте одну строку так, чтобы отказ не считался выполнением.
Отметка стоит до отправки, поэтому задание считается выполненным даже тогда, когда письмо не ушло. Достаточно поменять местами две строки — и добавить send в try, чтобы отказ до отметки не доходил:
Теперь первый проход кладёт в ящик одно письмо и оставляет одно задание ждать, а второй забирает и его: в ящике два, ждущих нет. Ровно этот порядок — «сделать, потом отметить» — и отличает журнал заданий от списка намерений.
Тем же одним шагом появляется и новая беда, ради которой берут пакет. Если процесс умрёт между send и update, письмо уйдёт дважды: отметки-то нет. Полностью это не лечится ничем — доставка «ровно один раз» через границу процесса невозможна, — но pg-boss и BullMQ дают то, что здесь пришлось бы писать самому: счётчик попыток, нарастающую паузу перед повтором и предел, после которого задание уходит в отдельный список, а не крутится вечно.