Node.js
Экспресс-шпаргалка 20/20
- Event Loop проходит фазы
timers -> pending callbacks -> poll -> check -> close, аmicrotasks(Promise,nextTick) выполняются между фазами. - Streams выбирают для больших payload и непрерывных данных, чтобы не держать все в памяти и использовать backpressure.
Bufferнужен для бинарных данных: файлы, сокеты, криптография, кодировки.nextTickвыполняется раньше всего,setImmediateвcheck,setTimeoutвtimersпосле минимальной задержки.worker_threadsдля CPU-bound задач,clusterдля масштабирования HTTP-процесса по ядрам.- CommonJS (
require) проще для legacy, ESM (import) стандартнее и лучше для современного tooling. - Централизованный error handling: единый error schema, middleware/filters, correlation id и разделение 4xx/5xx.
- Graceful shutdown: stop accepting new requests -> drain active -> close DB/broker -> forced exit by timeout.
- От перегрузки защищают rate limit, очереди, таймауты, circuit breaker и ограничение параллелизма.
- Логи должны быть структурными (JSON), с
traceId/requestId, уровнями и метриками latency/error-rate. - Конфиг:
12-factor, fail-fast валидация env при старте, секреты только через secret manager. - Большие файлы обрабатывают stream/pipeline с лимитами размера, type-check и защитой от медленных клиентов.
healthпоказывает жив ли процесс,readinessготов ли сервис принимать трафик с учетом зависимостей.- JWT безопасен только с проверкой
alg, подписи,exp/aud/iss, ротацией ключей и коротким TTL access-токена. - Кэш ставят на read-heavy пути с TTL, стратегией инвалидации и защитой от cache stampede.
- WebSocket требует heartbeat/ping-pong, лимитов соединений, backpressure и стратегии reconnect.
- Утечки памяти ищут через heap snapshots, allocation profiling и сравнение роста heap во времени.
- ORM ускоряет CRUD, query builder/raw SQL нужен для сложных и критичных по perf запросов.
- N+1 лечат batching, joins/eager loading и измерением количества запросов на один API call.
- Backward compatibility достигают additive changes, versioning, deprecation window и контрактными тестами.
1. Как устроен Event Loop в Node.js и его фазы?
Теги: nodejs, backend, event-loop, async
Сложность: Middle/Senior
Короткий ответ
Event Loop - механизм, который позволяет Node.js выполнять неблокирующий I/O на одном основном JavaScript-потоке. Он проходит фазы timers, pending callbacks, poll, check, close callbacks; process.nextTick и microtasks выполняются между переходами, поэтому могут влиять на порядок и latency.
Что сказать на интервью (30-60 секунд)
Node.js не делает ваш JavaScript параллельным автоматически: один долгий синхронный callback блокирует обработку всех остальных callbacks. На интервью я объясняю фазы через реальные события: timers для setTimeout/setInterval, poll для I/O, check для setImmediate, close для закрытия сокетов. Отдельно проговариваю, что timers задают минимальный порог, а не точное время. В проде это проверяется через event loop lag, p95/p99 latency и поиск CPU-bound кода в request path.
Мини-пример
console.log('A');
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
Вне I/O порядок timer и immediate может зависеть от контекста и версии Node.js, поэтому зрелый ответ не должен обещать стабильный порядок для всех случаев.
Пояснение
Event Loop - это не "цикл промисов", а очереди callbacks вокруг I/O. Типичный путь запроса такой: JavaScript быстро ставит I/O-операцию, освобождает поток, потом callback возвращается в нужную очередь. Если внутри callback сделать тяжелый JSON parse, синхронную криптографию или большой цикл, Node не сможет перейти к следующим callbacks.
На интервью важно связать фазы с симптомами. Если растет event loop lag, но CPU высоко, проблема часто в синхронной работе на основном потоке. Если CPU нормальный, но растет latency внешних вызовов, Event Loop может быть не причиной. Так вы показываете, что умеете диагностировать, а не просто перечислять фазы.
Углубление (2-3 минуты)
| Фаза | Что обычно выполняется | Интервью-ловушка |
|---|---|---|
timers | callbacks setTimeout/setInterval после минимального threshold | 0 ms не значит "сразу" |
pending callbacks | некоторые отложенные системные I/O callbacks | редко нужен в прикладном коде, но фаза существует |
poll | прием новых I/O событий и выполнение I/O callbacks | длинный callback задержит timers |
check | callbacks setImmediate | внутри I/O обычно раньше setTimeout(..., 0) |
close callbacks | закрытие handles, например socket close | важно для cleanup |
Практический сценарий: API принимает большой CSV, синхронно парсит его в handler и "иногда" тормозит весь сервис. Хороший ответ: ограничить размер, читать stream-ом, тяжелую CPU-обработку вынести в worker/очередь, добавить метрику event loop lag и latency по endpoint.
Практика
- Выпишите порядок вывода для 3 snippets: top-level
setTimeout/setImmediate, тот же код внутриfs.readFile, и код сPromise.resolve().then(...)плюсprocess.nextTick(...). - Научитесь объяснять, почему timer может выполниться позже указанного delay.
- Подготовьте production-кейс: "event loop lag вырос, CPU 95%, p99 latency растет" и план диагностики.
Типичные ошибки
- Говорят, что Node.js "однопоточный", и забывают про libuv/system kernel для I/O и worker pool для части операций.
- Обещают точный порядок
setTimeout(..., 0)иsetImmediateбез учета контекста. - Не отличают I/O-bound проблему от CPU-bound проблемы.
- Используют рекурсивный
process.nextTickи могут заstarve-ить I/O.
Follow-up вопросы
- Почему
setTimeout(fn, 0)не гарантирует немедленное выполнение? - Что произойдет с HTTP latency, если внутри handler сделать CPU-bound цикл на 300 ms?
- Чем event loop lag отличается от latency базы данных?
- Почему
setImmediateвнутри I/O callback обычно предсказуемееsetTimeout(..., 0)?
Что повторить
- Фазы Event Loop и роль
poll/check. - Microtasks,
process.nextTick,Promise. - Event loop lag, CPU profiling, flamegraph.
Связанные модули и карта
2. Когда использовать Streams вместо чтения файла целиком?
Теги: nodejs, backend, streams
Сложность: Middle/Senior
Короткий ответ
Streams используют, когда данные большие, непрерывные или идут между источником и потребителем с разной скоростью. Главное преимущество - обработка чанками и backpressure: медленный consumer может притормозить producer вместо бесконечного роста памяти.
Что сказать на интервью (30-60 секунд)
Я выбираю stream, когда payload нельзя безопасно держать целиком в памяти: загрузка файла, проксирование ответа, обработка логов, экспорт отчета. В зрелом ответе важно не просто сказать "экономит память", а объяснить backpressure: если Writable.write() возвращает false, producer должен дождаться drain. В реальном сервисе я использую pipeline, ставлю лимиты размера/таймауты и проверяю RSS memory под большим файлом.
Мини-пример
import { createReadStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';
await pipeline(
createReadStream('./big.log'),
createGzip(),
process.stdout,
);
pipeline удобнее ручного .pipe(), потому что корректно прокидывает ошибки и завершение цепочки.
Пояснение
readFile нормален для маленького config-файла или небольшого JSON, где проще прочитать все целиком и валидировать. Stream нужен, когда размер может быть десятки/сотни мегабайт, когда данные приходят по сети или когда вы делаете transform без полной материализации данных.
Backpressure - центральная часть ответа. Если consumer пишет медленнее, чем producer читает, Node должен остановить поток чтения. Иначе память будет расти до OOM. На интервью хороший кандидат показывает, что проверяет не только happy path, но и медленного клиента, обрыв соединения, ошибку transform и лимиты.
Углубление (2-3 минуты)
| Решение | Когда подходит | Риск |
|---|---|---|
readFile | маленький файл, нужен весь payload сразу | скачок памяти на больших данных |
Readable/Writable | файл, socket, HTTP response, лог | сложнее обработка ошибок |
Transform | gzip, CSV parser, нормализация строк | нужно соблюдать backpressure |
pipeline | production-цепочка streams | надо правильно обработать abort/cleanup |
Кейс: endpoint экспортирует архив заказов. Плохой вариант - собрать весь CSV в строку и потом gzip. Хороший вариант - cursor/page source -> Transform в CSV -> gzip -> response stream, с лимитом времени, обработкой abort от клиента и метриками bytes/duration/errors.
Практика
- Соберите checklist:
pipeline, обработка ошибок, abort клиента, лимит размера, timeout, memory RSS, slow consumer. - Объясните, что делать, если
.write()вернулfalse: остановить запись новых chunks доdrain. - Сравните память для
readFileи stream на большом файле и сформулируйте вывод для интервью.
Типичные ошибки
- Используют
.on('data')и вручную пишут в consumer, игнорируяwrite() === false. - Не обрабатывают ошибки на всех участниках stream chain.
- Считают streams универсально быстрее: для маленького payload они могут быть лишней сложностью.
- Не учитывают abort клиента при streaming response.
Follow-up вопросы
- Почему
pipelineчасто лучше цепочки.pipe().pipe()? - Что такое
highWaterMarkи как он связан с backpressure? - Как защититься от клиента, который очень медленно читает response?
- Когда
readFileвсе-таки нормальный выбор?
Что повторить
Readable,Writable,Transform,Duplex.stream.pipelineиstream/promises.- Backpressure,
highWaterMark,drain.
Связанные модули и карта
3. Что такое Buffer и где он нужен?
Теги: nodejs, backend, buffer
Сложность: Middle/Senior
Короткий ответ
Buffer - фиксированная последовательность байтов для бинарных данных в Node.js. Он нужен там, где строка недостаточна: файлы, TCP/HTTP chunks, криптография, изображения, архивы, кодировки и протоколы.
Что сказать на интервью (30-60 секунд)
Buffer представляет bytes, а не символы. Я использую его, когда работаю с бинарным payload или явно управляю encoding. Важные детали для интервью: Buffer.alloc безопасно инициализирует память, Buffer.from создает данные из строки/массива, а Buffer.byteLength(str, 'utf8') может отличаться от str.length. В production я всегда ставлю лимит размера payload, чтобы binary upload не превратился в memory pressure.
Мини-пример
const payload = Buffer.from('hello', 'utf8');
console.log(payload.toString('hex'));
const text = 'привет';
console.log(text.length);
console.log(Buffer.byteLength(text, 'utf8'));
Второй пример показывает границу: длина строки в JS и количество байтов в UTF-8 - разные вещи.
Пояснение
Buffer часто всплывает на интервью в вопросах про файлы, network protocols и безопасность. Нельзя отвечать только "это бинарные данные": нужно показать, что вы понимаете границы string/bytes/encoding. Например, если API принимает base64-файл, вы должны оценить размер после декодирования, проверить MIME/signature, не держать огромный Buffer в памяти без необходимости и лучше перейти к stream для больших данных.
Еще одна практическая деталь: Buffer является подклассом Uint8Array, но Node добавляет методы для типичных серверных задач. Поэтому в новом коде лучше явно импортировать Buffer из node:buffer, даже если глобально он доступен.
Углубление (2-3 минуты)
| Граница | Что проверить | Почему важно |
|---|---|---|
| string -> bytes | encoding и Buffer.byteLength | кириллица/emoji занимают больше 1 byte |
| bytes -> string | правильный toString(encoding) | иначе corrupted text |
| upload -> Buffer | лимит размера до декодирования и после | защита памяти |
| Buffer -> stream | нужен ли полный payload в памяти | большие файлы лучше stream-ить |
| binary protocol | offset, endianess, checksum | ошибка дает битый packet |
Кейс: сервис принимает файл в base64. Плохой ответ - просто Buffer.from(body.file, 'base64') без лимитов. Хороший ответ - проверить максимальный размер входной строки, оценить decoded size, валидировать тип, для больших файлов использовать streaming upload/storage, а не собирать весь файл в heap.
Практика
- Составьте boundary map:
HTTP body -> base64/string -> Buffer -> storage/stream. - Покажите, почему
str.lengthне равен размеру payload в bytes. - Подготовьте ответ, когда Buffer нужен, а когда лучше stream.
Типичные ошибки
- Сравнивают размер строки и размер файла через
length. - Декодируют base64 в память без лимита.
- Путают Buffer с JSON object или обычным массивом чисел.
- Не фиксируют encoding при преобразовании
string/Buffer.
Follow-up вопросы
- Чем
Buffer.byteLength('привет')отличается от'привет'.length? - Почему
Buffer.allocбезопаснее неинициализированного выделения памяти? - Когда binary payload лучше вести stream-ом?
- Как проверить, что загруженный файл действительно нужного типа?
Что повторить
Buffer.from,Buffer.alloc,Buffer.byteLength.- UTF-8, base64, hex.
- Лимиты upload payload и stream processing.
Связанные модули и карта
4. Разница между process.nextTick, setImmediate и setTimeout?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
process.nextTick выполняется после текущей операции до продолжения Event Loop, setImmediate попадает в check phase после poll, а setTimeout попадает в timers после минимального delay. Это разные инструменты планирования, а не взаимозаменяемые "задержки".
Что сказать на интервью (30-60 секунд)
Я бы объяснил через очередь: nextTick нужен для "сделать callback асинхронным, но до следующей фазы"; злоупотреблять им нельзя, потому что можно не дать Event Loop дойти до I/O. setImmediate удобен, когда нужно отложить работу до конца текущего I/O cycle. setTimeout(fn, 0) означает "не раньше минимального таймера", а не "немедленно". Для обычного userland deferral часто лучше queueMicrotask, если не нужны специфичные возможности nextTick.
Мини-пример
import { readFile } from 'node:fs';
readFile(import.meta.url, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
Внутри I/O callback setImmediate обычно выполнится раньше таймера с 0, потому что попадает в check phase после poll.
Пояснение
Этот вопрос часто проверяет не память на порядок вывода, а зрелость мышления. Если кандидат говорит "всегда будет такой порядок", это слабый сигнал. Правильнее говорить: порядок зависит от того, где планируются callbacks. Top-level setTimeout(..., 0) и setImmediate могут быть непредсказуемы, а внутри I/O callback setImmediate обычно выигрывает.
process.nextTick находится вне обычной диаграммы фаз: очередь nextTick очищается до продолжения Event Loop. Это полезно для API, который должен всегда вызывать callback асинхронно, но рекурсивный nextTick может заблокировать I/O.
Углубление (2-3 минуты)
| Инструмент | Когда выполнится | Хороший use case | Риск |
|---|---|---|---|
process.nextTick | до перехода Event Loop дальше | сделать callback гарантированно async в Node API | starvation I/O |
queueMicrotask | microtask queue | переносимый deferral для userland | ошибки и порядок надо понимать |
setImmediate | check phase | отложить работу после I/O callback | не путать с timer |
setTimeout(fn, 0) | timers после минимального delay | отложить минимум на следующий timer turn | не гарантирует точное время |
Кейс: библиотечная функция иногда вызывает callback синхронно, иногда после I/O. Это ломает потребителя. Решение - нормализовать callback как асинхронный, но не использовать бесконечный nextTick; для прикладной логики чаще достаточно queueMicrotask или setImmediate в зависимости от нужной очереди.
Практика
- Для каждого snippet предскажите вывод: top-level, внутри
fs.readFile, сPromise.then, сprocess.nextTick. - Объясните, почему рекурсивный
nextTickопасен. - Сформулируйте правило выбора:
nextTickдля Node API edge,setImmediateпосле I/O,setTimeoutдля real delay.
Типичные ошибки
- Называют
setTimeout(fn, 0)немедленным выполнением. - Считают
nextTickчастью обычных фаз Event Loop. - Используют
nextTickдля тяжелой работы. - Не учитывают различия CJS/ESM и microtask queue в edge cases.
Follow-up вопросы
- Почему
process.nextTickможет заstarve-ить I/O? - Что будет раньше внутри I/O callback:
setImmediateилиsetTimeout(..., 0)? - Когда вы выберете
queueMicrotaskвместоprocess.nextTick? - Почему timer delay - это threshold, а не SLA выполнения?
Что повторить
process.nextTickqueue.setImmediateиcheckphase.- Timers API и minimum threshold.
Связанные модули и карта
5. Когда выбирать worker_threads, а когда cluster?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
worker_threads выбирают для CPU-bound JavaScript-работы внутри одного Node.js процесса с возможностью передачи/разделения памяти. cluster выбирают для нескольких процессов Node.js, обычно чтобы распределить HTTP workload по ядрам и получить process isolation.
Что сказать на интервью (30-60 секунд)
Если задача CPU-bound, например image processing, PDF generation, crypto-heavy work или большой parse, я смотрю в сторону worker_threads или отдельной очереди workers, чтобы не блокировать Event Loop API-процесса. Если нужно обслуживать больше HTTP connections на нескольких CPU cores, подойдет несколько процессов: cluster, process manager или container replicas. Важно сказать, что workers не ускоряют обычный async I/O: встроенный I/O Node уже эффективнее, чем гонять его через worker.
Мини-пример
import { Worker } from 'node:worker_threads';
const worker = new Worker(new URL('./hash-worker.js', import.meta.url), {
workerData: { payload: 'large-input' },
});
worker.once('message', console.log);
worker.once('error', console.error);
Worker выносит CPU-heavy участок из основного Event Loop, но требует контроля очереди задач, ошибок и завершения.
Пояснение
worker_threads и cluster отвечают на разные вопросы. Worker thread - "как выполнить тяжелый JS parallel внутри процесса". Cluster - "как поднять несколько Node процессов, которые могут разделять server port". У cluster сильнее изоляция: падение одного worker process не обязано уронить primary/другие процессы, но память не общая. У worker_threads дешевле обмен через transfer/shared memory, но shared state сложнее и требует аккуратности.
Зрелый ответ обязательно включает альтернативы. В Kubernetes/PM2/systemd часто масштабирование HTTP процесса делают не вручную через cluster, а репликами процесса. Для CPU-heavy задач часто лучше отдельная очередь и worker service, если работа долгая и не должна жить в lifecycle HTTP request.
Углубление (2-3 минуты)
| Задача | Лучше выбрать | Почему |
|---|---|---|
| CPU-heavy parse/hash/image внутри сервиса | worker_threads | разгружает основной Event Loop |
| Много HTTP traffic на одном host | процессы/cluster/реплики | использует несколько CPU cores и дает изоляцию |
| Долгая фоновая задача с retry | queue + отдельный worker service | не держит HTTP request lifecycle |
| I/O-heavy запросы к DB/API | обычный async I/O + limits | worker threads почти не помогут |
| Нужна process isolation | cluster или отдельные процессы | падение изолируется лучше |
Кейс: endpoint генерирует PDF 2 секунды CPU и из-за этого p99 всего API растет. Варианты: быстро вернуть job id и отправить задачу в очередь; если нужен sync-response, ограничить concurrency и вынести генерацию в worker pool. Проверка: event loop lag основного процесса падает, p99 endpoint не убивает остальные маршруты, очередь не растет бесконечно.
Практика
- Составьте decision matrix: CPU-bound, I/O-bound, isolation, memory sharing, deployment model.
- Опишите worker pool с лимитом concurrency и backpressure для входящих задач.
- Подготовьте ответ, почему
worker_threadsне лечит медленную базу данных.
Типичные ошибки
- Запускают worker на каждый request без pool и получают overhead/DoS по CPU.
- Используют workers для обычного I/O и усложняют систему без выигрыша.
- Путают thread-level parallelism и process-level scaling.
- Не продумывают graceful shutdown: что делать с задачами в worker при остановке процесса.
Follow-up вопросы
- Почему
worker_threadsполезны для CPU-bound, но не для обычного I/O-bound? - Как ограничить количество задач в worker pool?
- Чем падение worker thread отличается от падения cluster worker process?
- Когда вместо
clusterлучше использовать несколько container replicas?
Что повторить
worker_threads:Worker,workerData,parentPort, transfer/shared memory.cluster: primary/worker processes и shared server port.- CPU-bound vs I/O-bound, worker pool, queue, graceful shutdown.
Связанные модули и карта
6. Разница CommonJS и ESM в Node.js?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
CommonJS - историческая модульная система Node.js с require и module.exports; ESM - стандарт ECMAScript с import/export. В Node.js выбор зависит от package.json type, расширений .cjs/.mjs, interop с зависимостями и поведения загрузки.
Что сказать на интервью (30-60 секунд)
Я бы отвечал не "ESM современнее", а через границы совместимости. CommonJS удобен в legacy Node-коде и загружает модули через require. ESM нужен для стандартного import/export, top-level await, browser/tooling compatibility и новых пакетов. В проде важнее всего не смешать форматы хаотично: явно указать type, использовать .cjs/.mjs для границ и проверить, как CJS импортирует ESM-зависимость и наоборот.
Мини-пример
// CommonJS
const legacy = require('./lib.cjs');
// ESM
import modern from './lib.mjs';
// В ESM можно загрузить CJS как default import.
import legacyPackage from './legacy.cjs';
Пояснение
Node.js поддерживает две системы модулей. Файл .cjs трактуется как CommonJS, .mjs - как ESM, а .js зависит от ближайшего package.json с полем type. Это типичный source of bugs при миграции: тесты проходят локально, а в CI пакет загружается другим loader-ом.
Interop тоже не симметричен. ESM может импортировать CommonJS, но CJS через require работает с ESM только при ограничениях, например без top-level await; универсальный путь для CJS -> ESM часто await import(...). Поэтому сильный ответ должен включать migration plan, а не лозунг "перейдем на ESM".
Углубление (2-3 минуты)
| Решение | Когда подходит | Проверка перед merge |
|---|---|---|
| Оставить CJS | legacy сервис, много CJS-зависимостей | нет ESM-only пакетов в critical path |
| Перейти на ESM | новый сервис, modern tooling, top-level await | type: "module", импорты с расширениями, тесты CI |
| Смешанный режим | постепенная миграция | явные .cjs/.mjs boundary files |
Dynamic import() | CJS должен загрузить ESM | обработать async boundary и ошибки загрузки |
Кейс: команда обновила библиотеку, она стала ESM-only, а сервис CommonJS падает на старте. Хороший план: найти точку импорта, заменить на dynamic import() или выделить ESM boundary, проверить cold start, тесты и сборку, затем решить, нужна ли полная миграция.
Практика
- Составьте module interop decision table для проекта:
type, entrypoint, test runner, build, CJS-зависимости, ESM-only зависимости. - Напишите 2 мини-примера: ESM импортирует CJS; CJS загружает ESM через dynamic
import(). - Объясните, почему
__dirnameв ESM не работает как в CJS и чем заменить.
Типичные ошибки
- Меняют
typeвpackage.jsonбез проверки всех entrypoints. - Думают, что
importиrequireотличаются только синтаксисом. - Ломают
__dirname,__filename, JSON/native imports или test runner при миграции. - Не проверяют ESM-only зависимости до обновления пакета.
Follow-up вопросы
- Как Node понимает, что
.jsфайл надо читать как ESM или CJS? - Что будет, если CJS попробует
requireESM-модуль с top-levelawait? - Как в ESM получить аналог
__dirname? - Как бы вы мигрировали большой CJS-сервис без остановки разработки?
Что повторить
- Node.js module resolution:
.cjs,.mjs,type. - ESM/CommonJS interop и dynamic
import(). - Отличия module scope в CJS и ESM.
Связанные модули и карта
7. Как организовать централизованный error handling в Node API?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Централизованный error handling в Node API строится вокруг единой классификации ошибок, middleware/filter на границе HTTP, стабильного response envelope, логирования с request/trace id и запрета утечки внутренних деталей клиенту.
Что сказать на интервью (30-60 секунд)
Я разделяю ошибки на ожидаемые доменные, валидационные, auth/permission, dependency failures и unexpected 500. На HTTP-границе маплю их в статус, код ошибки и безопасное сообщение. В Express это обычно последний error middleware, куда попадают next(err) и rejected async handlers. Важно логировать stack и context внутри сервиса, но клиенту отдавать стабильный контракт без stack trace и секретов.
Мини-пример
import express from 'express';
const app = express();
app.get('/users/:id', async (req, res) => {
const user = await loadUser(req.params.id);
if (!user) throw new NotFoundError('USER_NOT_FOUND');
res.json(user);
});
app.use((err, req, res, _next) => {
const mapped = mapError(err);
req.log.error({ err, requestId: req.id }, 'request failed');
res.status(mapped.status).json({ error: mapped.body });
});
Пояснение
Цель не в том, чтобы "поймать все ошибки", а в том, чтобы каждый тип сбоя превращался в предсказуемое поведение. Например, ошибка валидации должна дать 400 с понятным полем, отсутствие ресурса - 404, конфликт версии - 409, недоступность зависимости - часто 502/503, а неожиданный bug - 500 с generic message.
Отдельная ловушка - async errors. В Express 5 rejected Promise из route handler автоматически идет в next, но в старом коде и callbacks нужно явно передавать ошибку в next(err). Еще одна ловушка - двойной response: если headers уже отправлены, error middleware не должен пытаться отправить новый JSON.
Углубление (2-3 минуты)
| Ошибка | HTTP | Что клиенту | Что в лог |
|---|---|---|---|
| Validation | 400 | field-level code/message | invalid fields без sensitive values |
| AuthN/AuthZ | 401/403 | stable error code | subject, route, policy result |
| Not found | 404 | resource-specific code | lookup keys без секретов |
| Conflict | 409 | retry/change input hint | version/idempotency context |
| Dependency timeout | 502/503/504 | temporary failure code | dependency, timeout, attempt |
| Unexpected bug | 500 | generic message | stack, request id, release version |
Кейс: платежная интеграция иногда отвечает timeout. Плохой ответ - вернуть 500 и stack наружу. Хороший ответ - timeout классифицируется как dependency failure, клиент получает стабильный код, retry policy зависит от идемпотентности операции, в логах есть trace id и dependency latency.
Практика
- Составьте API error envelope mapping: domain error -> status -> public code -> log context.
- Разберите 5 ошибок: validation, not found, duplicate, dependency timeout, unexpected throw.
- Напишите правило: какие поля нельзя отдавать клиенту и какие обязательно нужны в логах.
Типичные ошибки
- Отдают клиенту raw
err.messageи stack trace. - Все ошибки превращают в
500, из-за чего клиент не может правильно реагировать. - Логируют без request/trace id и потом не могут связать ошибку с запросом.
- Забывают async/callback errors и получают unhandled rejection или зависший request.
Follow-up вопросы
- Чем operational error отличается от programmer error?
- Что делать, если ошибка произошла после отправки headers?
- Как сохранить backward compatibility error envelope?
- Какие ошибки можно retry-ить, а какие нельзя?
Что повторить
- Express error middleware и async handler behavior.
- HTTP status mapping: 400/401/403/404/409/429/5xx.
- Correlation id, trace id, безопасное логирование.
Связанные модули и карта
8. Как реализовать graceful shutdown сервиса?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Graceful shutdown - управляемое завершение: получить сигнал, перестать принимать новые запросы, дать активным операциям закончиться в пределах deadline, закрыть HTTP server, DB/broker/resources и только потом завершить процесс.
Что сказать на интервью (30-60 секунд)
Я описываю shutdown как drain protocol. На SIGTERM сервис переводит readiness в not ready, останавливает прием новых соединений, вызывает server.close, ждет активные запросы и фоновые задачи, закрывает DB/broker, а после таймаута принудительно завершает процесс. В Node важно помнить про keep-alive connections и upgraded sockets: иногда нужны closeIdleConnections или осторожный closeAllConnections после deadline.
Мини-пример
process.on('SIGTERM', async () => {
server.close(async () => {
await db.close();
process.exit(0);
});
setTimeout(() => {
server.closeAllConnections();
process.exit(1);
}, 30_000).unref();
});
Пояснение
server.close перестает принимать новые соединения и завершает сервер после закрытия активных. Это не равно "сразу убить процесс". На интервью важно сказать, что shutdown должен быть идемпотентным: SIGTERM может прийти повторно, а cleanup не должен запускаться дважды.
Главный production-риск - потерять in-flight операции или зависнуть навсегда. Поэтому нужен deadline: например, 25-30 секунд под orchestrator grace period. Для очередей и фоновых задач отдельно решают, что делать: закончить текущую задачу, вернуть job в очередь, сохранить checkpoint или отменить через AbortController.
Углубление (2-3 минуты)
| Шаг | Что сделать | Проверка |
|---|---|---|
| Signal | обработать SIGTERM/SIGINT один раз | повторный сигнал не ломает cleanup |
| Readiness | убрать instance из трафика | новый traffic не идет на pod/process |
| HTTP drain | server.close, active requests завершаются | p99 не обрывается массово |
| Resources | закрыть DB, cache, broker, timers | процесс не висит из-за handles |
| Deadline | forced close/exit после таймаута | deploy не зависает бесконечно |
Кейс: rolling deploy обрывает платежные requests. Хороший ответ: readiness сначала, потом drain HTTP, идемпотентные операции через idempotency key, фоновые job не удаляются до ack, в логах есть shutdown phase и counts активных операций.
Практика
- Составьте shutdown drain checklist для HTTP + DB + queue.
- Опишите, что будет при активном keep-alive соединении и долгом request.
- Добавьте readiness-состояния:
ready,draining,stopped.
Типичные ошибки
- Сразу вызывают
process.exit(0)и режут active requests. - Не ставят общий shutdown deadline.
- Не закрывают keep-alive/idle/upgraded connections.
- Забывают про фоновые timers, consumers и DB pool.
Follow-up вопросы
- Чем readiness отличается от liveness при shutdown?
- Что делать с WebSocket или HTTP upgraded connections?
- Как защититься от повторного
SIGTERM? - Как проверить graceful shutdown в e2e или staging?
Что повторить
- Node.js signal events.
http.Server.close,closeIdleConnections,closeAllConnections.- Readiness/liveness и orchestrator grace period.
Связанные модули и карта
9. Как защитить API от перегрузки (rate limit/backpressure)?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Защита от перегрузки - это набор лимитов на вход, работу и выход: rate limit, timeouts, concurrency limits, queue/backpressure, payload limits, circuit breaker и честная деградация вместо бесконечного накопления запросов.
Что сказать на интервью (30-60 секунд)
В Node API перегрузка часто проявляется как рост event loop lag, memory RSS, pending requests и timeouts зависимостей. Я защищаюсь слоями: reverse proxy/WAF, rate limit по ключу, лимит payload, HTTP headers/request timeouts, ограничение concurrency на дорогих операциях, очередь с max length и быстрый 429/503, если система не успевает. Backpressure означает "притормозить или отказать", а не просто складывать все в память.
Мини-пример
const MAX_IN_FLIGHT = 50;
let inFlight = 0;
app.use((req, res, next) => {
if (inFlight >= MAX_IN_FLIGHT) {
return res.status(503).json({ error: { code: 'OVERLOADED' } });
}
inFlight += 1;
res.on('finish', () => { inFlight -= 1; });
next();
});
Это упрощенный пример concurrency guard; в реальном сервисе лимиты обычно распределены по route, user/key, dependency и worker pool.
Пояснение
Rate limit отвечает на вопрос "сколько запросов разрешить от клиента за период". Backpressure отвечает на вопрос "что делать, когда downstream или сам сервис уже не успевает". Timeouts отвечают на вопрос "сколько ждать". Нужны все три, потому что rate limit не спасет от медленной базы, а timeout без concurrency limit может создать шторм ретраев.
Node.js особенно чувствителен к тяжелой работе на Event Loop. Официальное правило простое: работа на одного клиента должна оставаться маленькой. Если каждый запрос делает дорогой sync JSON parse, regex или CPU-heavy transform, никакой rate limit не спасет без выноса работы или ограничения concurrency.
Углубление (2-3 минуты)
| Риск | Контроль | Сигнал |
|---|---|---|
| Слишком много клиентов | rate limit, quota, auth key limits | 429, requests per key |
| Дорогая операция | concurrency limit, worker pool, queue | in-flight, queue depth |
| Медленная зависимость | timeout, circuit breaker, fallback | dependency latency/error rate |
| Большой payload | body size limit, stream, backpressure | RSS memory, rejected payloads |
| Slowloris/медленные headers | headersTimeout, proxy timeout | open sockets, 408 |
| Retry storm | retry budget, jitter, idempotency | retry count, 503/504 |
Кейс: внешний API начинает отвечать по 10 секунд, ваш сервис держит тысячи pending requests и падает по памяти. Хороший ответ: outbound timeout, concurrency limit на dependency, circuit breaker, быстрый fallback/503, метрики queue depth и saturation, плюс retry policy только для идемпотентных операций.
Практика
- Составьте overload control matrix: вход, route, dependency, queue, payload, socket.
- Для каждого контроля укажите отказ:
429,503, timeout, fallback или enqueue. - Опишите, какие метрики покажут перегрузку до падения процесса.
Типичные ошибки
- Ставят только rate limit и забывают dependency/concurrency limits.
- Делают бесконечную очередь в памяти.
- Ретраят все ошибки без jitter и idempotency.
- Не ограничивают body size и headers/request timeouts.
Follow-up вопросы
- Чем
429отличается от503в перегрузке? - Когда лучше отказать сразу, чем ставить запрос в очередь?
- Как timeout может усилить retry storm?
- Какие метрики покажут saturation Node-процесса?
Что повторить
- Event loop lag и "do not block the event loop".
- HTTP timeouts: headers/request/keep-alive.
- Backpressure, queue depth, concurrency limits.
Связанные модули и карта
10. Как проектировать структуру логирования и трассировки?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Логирование отвечает "что произошло", трассировка отвечает "какой путь прошел запрос", метрики отвечают "как меняется система во времени". В Node API нужны structured logs, request/trace id, span context, latency/error fields и запрет на секреты/PII.
Что сказать на интервью (30-60 секунд)
Я проектирую observability от вопросов, которые нужно быстро отвечать при инциденте: какой request упал, какая зависимость тормозит, какой пользовательский сценарий затронут, какой release это принес. В логах храню structured JSON с level, message, requestId/traceId, route, status, duration, error code. В трассировке span показывает операцию и ее children: HTTP handler, DB query, external call, queue produce/consume. Секреты и raw payload не логируются.
Мини-пример
req.log.info({
requestId: req.id,
traceId: req.traceId,
route: 'GET /users/:id',
statusCode: res.statusCode,
durationMs,
}, 'request completed');
Пояснение
Хороший ответ не сводится к "поставить Winston/Pino". Библиотека вторична; важна схема сигналов. Лог должен быть машинно-читаемым и коррелируемым с trace. Trace должен показывать причинную цепочку, а span - единицу работы с start/end time, attributes и status. Метрики должны давать агрегаты: RPS, p95/p99 latency, error rate, saturation, queue depth.
В Node-коде отдельная сложность - async context propagation. Если request id теряется между middleware, promise, callback или queue, расследование разваливается. Поэтому стоит явно проверять, что request/trace id проходит через async boundaries и external calls.
Углубление (2-3 минуты)
| Сигнал | Что хранить | Зачем |
|---|---|---|
| Log | event, level, requestId, route, error code | расследовать конкретное событие |
| Trace | trace id, spans, parent/child, status | увидеть путь запроса по сервисам |
| Metric | latency, error rate, saturation, throughput | увидеть тренд и алерт |
| Event | business milestone без PII | понять влияние на сценарий |
Кейс: пользователи жалуются на sporadic 5xx. Сильный ответ: по error rate алертим, по trace id находим route, в span видим external call timeout, в логах есть public error code и dependency attempt, в метриках подтверждаем рост p99 latency именно у dependency.
Практика
- Составьте logging/tracing signal map для одного endpoint: request start, DB call, external call, response, error.
- Укажите, какие поля обязательны, какие запрещены, какие имеют high-cardinality риск.
- Подготовьте 60-секундный incident walkthrough от alert до root cause.
Типичные ошибки
- Пишут plain text logs без request id и structured fields.
- Логируют raw body, токены, cookies или персональные данные.
- Путают logs, metrics и traces и пытаются одним сигналом заменить все.
- Добавляют high-cardinality labels в метрики, например user id.
Follow-up вопросы
- Чем log отличается от trace span?
- Какие поля нужны в error log для расследования?
- Почему user id опасен как metric label?
- Как проверить, что trace id не теряется в async code?
Что повторить
- OpenTelemetry traces, spans, context propagation.
- Structured logging и redaction.
- RED/USE style metrics: rate, errors, duration, saturation.
Связанные модули и карта
11. Как выстроить конфигурацию окружений и работу с secrets?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Конфигурацию в Node-сервисе держат во внешней среде, валидируют при старте и читают через типизированный config layer. Secrets не коммитят в репозиторий и не логируют; их хранят в secret manager или защищенной runtime-среде с ротацией.
Что сказать на интервью (30-60 секунд)
Я бы начал с fail-fast: если DATABASE_URL, JWT_ISSUER или timeout заданы неверно, сервис должен упасть на старте, а не ломаться под первым запросом. process.env в Node хранит строки, поэтому числа, boolean и списки нужно явно парсить и валидировать. Secrets отделяются от обычных config flags: у них другой доступ, аудит, ротация и redaction в логах.
Мини-пример
const config = {
port: Number.parseInt(process.env.PORT ?? '', 10),
jwtIssuer: process.env.JWT_ISSUER,
requestTimeoutMs: Number.parseInt(process.env.REQUEST_TIMEOUT_MS ?? '', 10),
};
if (!Number.isInteger(config.port) || !config.jwtIssuer) {
throw new Error('Invalid runtime configuration');
}
Пояснение
Главная мысль: env - это transport, а не типовая система. Все значения приходят как строки и должны пройти schema validation. Хороший config layer возвращает уже нормализованные значения: number, boolean, enum, URL, duration. Тогда прикладной код не делает process.env.X в случайных местах и не размазывает parsing bugs по сервису.
Secrets - отдельная категория. Access token, private key, DB password, webhook secret нельзя хранить в .env.example, логах, метриках, client bundle или issue-трекере. На интервью полезно проговорить lifecycle: provision -> use -> rotate -> revoke -> audit.
Углубление (2-3 минуты)
| Тип значения | Пример | Проверка |
|---|---|---|
| Required env | DATABASE_URL | есть, валидный URL |
| Number | PORT, TIMEOUT_MS | integer, диапазон |
| Enum | NODE_ENV, LOG_LEVEL | только разрешенные значения |
| Boolean | FEATURE_X_ENABLED | явный parser, не truthy string |
| Secret | JWT_PRIVATE_KEY | не логируется, доступ ограничен, есть ротация |
Кейс: CACHE_TTL_SECONDS="0" случайно трактуется как truthy string и включает кэш без TTL. Сильный ответ: config schema парсит number, проверяет min/max, падает на старте и имеет тест на production env sample.
Практика
- Составьте env validation checklist: required, type, range, default, secret/redaction, owner.
- Объясните, почему
process.env.FEATURE_ENABLED === 'false'опасно проверять как boolean. - Подготовьте runbook ротации секрета: новый ключ, dual-read/dual-sign, rollout, revoke old.
Типичные ошибки
- Читают
process.envпрямо в бизнес-логике. - Не валидируют env при старте и получают runtime-инцидент.
- Логируют secrets в error context или config dump.
- Путают public config и secret config.
Follow-up вопросы
- Почему
process.envнедостаточно как config layer? - Как безопасно ротировать JWT signing key?
- Какие env значения нельзя иметь с default?
- Что должно быть в
.env.example, а чего там быть не должно?
Что повторить
- Node.js
process.envи env files. - 12-factor config principle.
- Secret redaction, rotation, least privilege.
Связанные модули и карта
12. Как обрабатывать загрузку больших файлов в Node?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Большие загрузки в Node обрабатывают streaming pipeline-ом с лимитами размера, типа, времени и скорости чтения. Нельзя бездумно собирать весь файл в Buffer или память процесса: это ведет к OOM и блокировке остальных запросов.
Что сказать на интервью (30-60 секунд)
Я описываю upload как pipeline: request stream -> size/type validation -> optional transform/virus scan -> object storage/temp file -> metadata commit. На каждом шаге нужны abort handling, backpressure, cleanup temp files, лимит concurrency и timeout. Если файл нужен для CPU-heavy обработки, сам upload не должен держать HTTP request до конца обработки: лучше вернуть job id и обрабатывать в фоне.
Мини-пример
import { pipeline } from 'node:stream/promises';
import { createWriteStream } from 'node:fs';
app.post('/upload', async (req, res, next) => {
try {
await pipeline(req, createWriteStream('/tmp/upload.bin'));
res.status(201).json({ ok: true });
} catch (error) {
next(error);
}
});
Мини-пример показывает потоковую запись, но в production к нему обязательно добавляют size limit, type validation, auth, cleanup и storage policy.
Пояснение
Большая загрузка ломается не только размером. Есть slow client, upload abort, неправильный Content-Type, zip bomb, слишком много параллельных загрузок, медленный storage и CPU-heavy post-processing. Поэтому зрелый ответ должен включать guards, а не только "использовать streams".
Backpressure важен и здесь: если storage пишет медленнее, чем клиент шлет данные, pipeline должен естественно притормозить чтение. Но это не отменяет лимитов, потому что медленный клиент может держать соединение слишком долго.
Углубление (2-3 минуты)
| Guard | Что проверяет | Ошибка без него |
|---|---|---|
| Auth/permission | кто может грузить | storage abuse |
| Size limit | bytes до/во время чтения | OOM, переполнение диска |
| Type/signature | MIME и magic bytes | вредный или битый файл |
| Timeout/rate | слишком медленный клиент | занятые sockets |
| Backpressure | скорость consumer | рост памяти |
| Cleanup | temp file/object | мусор и cost leak |
| Async processing | тяжелая обработка | блокировка request path |
Кейс: пользователи грузят видео, а сервис сначала собирает весь файл в память и потом отправляет в storage. Под нагрузкой RSS растет и процесс падает. Хороший ответ: streaming upload напрямую в storage/temp file, лимит размера, abort cleanup, очередь на transcode, отдельные метрики upload bytes/duration/errors.
Практика
- Составьте large-upload pipeline guard list: auth, size, type, timeout, storage, cleanup, async job.
- Объясните, что делать при
abortedrequest. - Назовите метрики: active uploads, bytes uploaded, duration, rejected by size/type, storage error rate.
Типичные ошибки
- Используют in-memory upload для больших файлов.
- Проверяют только
Content-Type, но не signature/размер. - Не удаляют temp file при abort/error.
- Делают CPU-heavy обработку прямо в HTTP handler.
Follow-up вопросы
- Чем multipart upload отличается от JSON base64 upload по рискам памяти?
- Как защититься от slow upload клиента?
- Почему
pipelineлучше ручной связки listeners? - Когда нужен pre-signed upload напрямую в object storage?
Что повторить
stream.pipelineи backpressure.- HTTP request timeouts и abort handling.
- File type validation, temp file cleanup, object storage flow.
Связанные модули и карта
13. Зачем и как добавлять healthcheck/readiness endpoints?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Liveness/health отвечает, жив ли процесс; readiness отвечает, можно ли отправлять на него трафик прямо сейчас. Readiness учитывает startup, shutdown/draining и критичные зависимости, а health endpoint должен быть быстрым и не превращаться в дорогой мониторинг.
Что сказать на интервью (30-60 секунд)
Я разделяю endpoints: live - процесс не завис и может быть перезапущен orchestrator-ом; ready - instance готов принимать production traffic. Во время startup readiness false, после прогрева true, при SIGTERM снова false и сервис уходит в drain. В readiness можно включать только критичные зависимости с коротким timeout; иначе healthcheck сам станет причиной перегрузки.
Мини-пример
let draining = false;
app.get('/live', (_req, res) => res.sendStatus(200));
app.get('/ready', async (_req, res) => {
if (draining) return res.sendStatus(503);
const dbOk = await checkDb({ timeoutMs: 200 });
res.sendStatus(dbOk ? 200 : 503);
});
Пояснение
Health endpoints нужны не пользователю, а платформе и on-call. Ошибка дизайна - сделать один /health, который дергает все зависимости без timeout. Тогда при проблеме базы healthcheck начинает создавать дополнительную нагрузку и ускоряет инцидент.
Readiness должна отражать routing decision: можно ли отправлять новые запросы на этот instance. Liveness должна быть осторожной: если она слишком строгая и падает из-за временной проблемы dependency, orchestrator начнет перезапускать здоровые процессы и усилит outage.
Углубление (2-3 минуты)
| Endpoint | Отвечает на вопрос | Что проверять |
|---|---|---|
/live | процесс жив? | event loop responsive, fatal state отсутствует |
/ready | можно слать traffic? | startup done, not draining, critical deps ok |
/startup | сервис прогрелся? | migrations/config/warmup завершены |
/metrics | как работает система? | отдельный endpoint, не health |
Кейс: база деградирует, /live проверяет базу и падает, Kubernetes рестартит все pods, cold starts добивают систему. Хороший ответ: dependency check живет в readiness с timeout и cache/jitter, liveness проверяет процесс, а алерты идут по метрикам и error rate.
Практика
- Составьте health/readiness dependency matrix: DB, cache, broker, external API, local disk.
- Для каждой зависимости решите: влияет на readiness или только на метрики.
- Опишите поведение endpoint во время startup, normal, dependency degraded, draining.
Типичные ошибки
- Один endpoint одновременно для liveness, readiness и diagnostics.
- Healthcheck делает дорогие запросы без timeout.
- Liveness зависит от внешней базы и вызывает restart storm.
- При shutdown readiness остается
200, и traffic продолжает идти на draining instance.
Follow-up вопросы
- Почему readiness должна стать false до
server.close? - Какие зависимости нельзя проверять в liveness?
- Как избежать thundering herd от health checks?
- Чем
/metricsотличается от/ready?
Что повторить
- Liveness, readiness, startup probes.
- Shutdown draining lifecycle.
- Dependency timeout, cached health status, alerting by metrics.
Связанные модули и карта
14. Как безопасно работать с JWT на backend?
Теги: nodejs, backend, security, auth
Сложность: Middle/Senior
Короткий ответ
JWT на backend безопасен только при строгой проверке подписи, alg, exp, nbf, iss, aud, subject/permissions и key rotation. Токен нельзя "декодировать и доверять"; decode без verify - это только чтение payload.
Что сказать на интервью (30-60 секунд)
Я проговариваю JWT как signed claims, а не как session storage. Backend должен выбрать допустимые algorithms, найти ключ по kid, проверить подпись и обязательные claims, потом выполнить authorization отдельно. Access token короткоживущий, refresh/revoke стратегия отдельная. Нельзя хранить секреты в frontend, принимать alg: none, доверять role без проверки issuer/audience или логировать токены.
Мини-пример
const payload = await verifyJwt(token, {
issuer: 'https://auth.example',
audience: 'api',
algorithms: ['RS256'],
});
if (!payload.sub) throw new AuthError('INVALID_SUBJECT');
Пояснение
JWT состоит из header, payload и signature. Header и payload легко декодируются любым клиентом, поэтому там не должно быть секретов. Без проверки подписи payload не имеет доверия. exp защищает срок жизни, iss и aud защищают от токена "не от того issuer" или "не для этого API".
Authentication и authorization - разные шаги. JWT может доказать, кто пользователь и какие claims выдал issuer, но решение "можно ли удалить ресурс" должно учитывать policy, ownership, scopes/roles и состояние ресурса.
Углубление (2-3 минуты)
| Проверка | Зачем |
|---|---|
alg allowlist | не принять слабый/неожиданный алгоритм |
| Signature | убедиться, что claims не подделаны |
exp/nbf | ограничить время действия |
iss | принять только доверенного issuer |
aud | убедиться, что токен предназначен этому API |
kid + JWKS cache | выбрать ключ и поддержать rotation |
| scopes/roles/policy | отделить authn от authz |
Кейс: сервис проверял только decode payload и роль admin. Атакующий меняет payload локально и получает доступ. Сильный ответ: always verify signature, issuer/audience, algorithm allowlist, короткий TTL, refresh token rotation, revoke strategy для критичных событий.
Практика
- Составьте JWT verification checklist: token source, alg, key, signature, exp, iss, aud, subject, scopes.
- Объясните, почему
jwt.decodeне равенjwt.verify. - Спроектируйте key rotation:
kid, JWKS cache TTL, old/new keys overlap, forced revoke.
Типичные ошибки
- Доверяют decoded payload без signature verification.
- Не проверяют
aud/issи принимают чужой токен. - Хранят секреты или PII внутри JWT payload.
- Путают role claim с полной authorization policy.
Follow-up вопросы
- Чем access token отличается от refresh token?
- Как отозвать JWT до
exp? - Что такое
kidи зачем нужен JWKS cache? - Почему JWT payload нельзя считать приватным?
Что повторить
- JWT structure: header, payload, signature.
iss,aud,exp,nbf,sub,kid.- Access/refresh tokens, revoke, rotation, authn vs authz.
Связанные модули и карта
15. Как организовать кэширование на стороне Node-сервиса?
Теги: nodejs, backend, cache
Сложность: Middle/Senior
Короткий ответ
Кэш в Node-сервисе ускоряет read-heavy path, но требует явной стратегии: cache-aside/read-through, TTL, invalidation, key design, serialization, stampede protection и метрики hit/miss/stale/error.
Что сказать на интервью (30-60 секунд)
Я не ставлю кэш "на всякий случай": сначала выбираю hot read path, допустимую stale-ness и источник истины. Потом определяю ключ, TTL, invalidation event, защиту от stampede и поведение при падении кэша. Для одного процесса можно in-memory LRU, для нескольких instances нужен внешний кэш вроде Redis; иначе у каждого процесса будет своя версия данных.
Мини-пример
async function getUserProfile(userId) {
const key = `user-profile:${userId}`;
const cached = await cache.get(key);
if (cached) return JSON.parse(cached);
const profile = await db.userProfile.findById(userId);
await cache.set(key, JSON.stringify(profile), { ttl: 60 });
return profile;
}
Пояснение
Кэш - это копия, а не источник истины. Главный вопрос интервью: какую несогласованность вы готовы терпеть? Для публичного каталога TTL 5 минут может быть нормален; для баланса счета stale data недопустима. Поэтому ответ должен начинаться с data semantics.
Stampede возникает, когда популярный ключ истек и много запросов одновременно идет в базу. Защита: singleflight/lock, stale-while-revalidate, jitter к TTL, prewarm, request coalescing. Отдельно важно не забыть negative caching: кэшировать "not found" можно, но с коротким TTL и осторожно.
Углубление (2-3 минуты)
| Решение | Когда подходит | Риск |
|---|---|---|
| In-memory LRU | один process, локальные справочники | разные instances видят разные данные |
| Redis/cache-aside | read-heavy данные между instances | stale data, network latency |
| TTL only | данные могут устаревать | stale window |
| Explicit invalidation | важна свежесть после write | сложнее события и race conditions |
| Stale-while-revalidate | лучше latency при истечении | надо маркировать stale |
| Singleflight/lock | защита от stampede | риск deadlock/lock timeout |
Кейс: карточка товара популярна, TTL истекает, 1000 запросов одновременно бьют в базу. Сильный ответ: TTL jitter, singleflight на ключ, stale-while-revalidate, ограничение concurrency к DB, метрики hit rate и backend fallback latency.
Практика
- Составьте cache invalidation/stampede matrix: key, TTL, invalidation trigger, stale tolerance, stampede guard.
- Разберите 3 типа данных: профиль пользователя, каталог, permissions.
- Назовите метрики: hit rate, miss rate, stale served, set/get errors, eviction, lock wait.
Типичные ошибки
- Не определяют источник истины и допустимую stale-ness.
- Делают key без version/tenant/user scope и смешивают данные.
- Не защищаются от cache stampede.
- Считают внешний кэш always available и не проектируют fallback.
Follow-up вопросы
- Чем cache-aside отличается от read-through?
- Когда in-memory cache опасен в multi-instance сервисе?
- Как инвалидировать кэш после write?
- Что делать при падении Redis: fail open или fail closed?
Что повторить
- Cache-aside, TTL, invalidation.
- Stampede protection: lock, singleflight, stale-while-revalidate, jitter.
- Cache key design, serialization, multi-instance consistency.
Связанные модули и карта
16. Как реализовать WebSocket и какие у него риски?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
WebSocket дает долгоживущий двунаправленный канал поверх HTTP upgrade. Его риски: авторизация соединения, heartbeat, reconnect, fan-out, лимиты подключений, backpressure, order/idempotency сообщений и корректное закрытие при деплое.
Что сказать на интервью (30-60 секунд)
Я бы объяснил, что WebSocket подходит для server push и интерактивных сценариев, где polling слишком дорогой или медленный. Но это не просто "быстрый REST": соединение живет долго, его надо аутентифицировать, отслеживать heartbeat-ом, ограничивать по пользователю/IP, уметь восстанавливать после reconnect и не складывать бесконечную очередь сообщений в память. В браузерном WebSocket API нет полноценного backpressure, поэтому сервер должен сам следить за buffered messages и slow clients.
Мини-пример
socket.on('message', async (raw) => {
const msg = parseAndValidate(raw);
if (!canPublish(socket.user, msg.channel)) {
return socket.close(1008, 'policy violation');
}
await publish(msg);
});
Пояснение
Сильный ответ включает lifecycle. Handshake проходит через HTTP upgrade, дальше канал живет отдельно от обычного request/response. Авторизация на handshake не заменяет authorization каждого action: пользователь может быть подключен, но не иметь права писать в конкретный room/channel.
Еще важнее reliability. Клиент может потерять связь, получить сообщение дважды, пропустить событие или подключиться к другому instance. Поэтому нужны message id, ack/resume strategy, idempotent handlers и external pub/sub, если соединения распределены между несколькими Node-процессами.
Углубление (2-3 минуты)
| Риск | Контроль | Проверка |
|---|---|---|
| Мертвые соединения | ping/pong heartbeat | stale sockets закрываются |
| Slow client | лимит buffered messages | память не растет без лимита |
| Fan-out overload | rate limit, batching, pub/sub | broadcast не блокирует Event Loop |
| Потеря сообщений | message id, ack, resume | reconnect не ломает сценарий |
| Multi-instance | sticky session или shared pub/sub | room state не только в памяти |
| Deploy/shutdown | drain, close code, reconnect hint | клиенты переподключаются корректно |
Кейс: чат работает на одном instance, потом его масштабировали до трех процессов. Пользователи в разных процессах перестали видеть сообщения друг друга. Хороший ответ: вынести room membership/pub-sub state во внешний слой, решить sticky sessions, добавить message id/ack и метрики active connections/fan-out lag.
Практика
- Составьте WebSocket reliability checklist: auth, heartbeat, limits, backpressure, reconnect, ordering, shutdown.
- Опишите reconnect flow: last seen message id, resume, missed messages, duplicate handling.
- Назовите метрики: active connections, messages/sec, buffered messages, reconnect rate, close codes.
Типичные ошибки
- Хранят весь state rooms только в памяти одного процесса.
- Не закрывают мертвые соединения и получают утечку sockets.
- Не ограничивают slow clients и buffered messages.
- Считают WebSocket заменой всем REST endpoints.
Follow-up вопросы
- Когда выбрать SSE вместо WebSocket?
- Как масштабировать rooms между несколькими Node instances?
- Что делать, если клиент пропустил сообщения во время reconnect?
- Как graceful shutdown влияет на WebSocket connections?
Что повторить
- WebSocket handshake, close codes, ping/pong.
- Backpressure/slow clients и message buffering.
- Pub/sub, sticky sessions, reconnect/resume.
Связанные модули и карта
17. Как отлавливать утечки памяти в Node приложении?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Утечку памяти в Node ищут по устойчивому росту heap/RSS после GC, а не по одному всплеску. Инструменты: метрики памяти, heap snapshots, allocation profiling, GC traces, сравнение snapshot-ов и поиск retaining paths.
Что сказать на интервью (30-60 секунд)
Я сначала отличаю leak от нормального роста под нагрузкой: смотрю heap used после GC, RSS, old space, event loop lag и restart/OOM history. Потом воспроизвожу сценарий, беру baseline heap snapshot, гоняю подозрительный flow, беру второй snapshot и сравниваю retained objects. Heap snapshot в production опасен: он останавливает main thread и может резко увеличить память, поэтому делать его надо на disposable instance или через безопасный runbook.
Мини-пример
const sessions = new Map();
app.post('/login', (req, res) => {
sessions.set(req.user.id, { user: req.user, createdAt: Date.now() });
res.sendStatus(204);
});
Мини-пример показывает типичный leak: Map растет бесконечно, если нет TTL, logout cleanup или лимита.
Пояснение
Типичные причины: глобальные caches без TTL, listeners без remove, timers, замыкания на большие объекты, очереди без max size, WebSocket/session maps, неосвобожденные native resources. Не каждый рост памяти - leak: V8 может держать heap для будущих аллокаций, а RSS может расти из-за native buffers.
Сильный ответ показывает порядок диагностики: метрики -> гипотеза -> воспроизведение -> snapshot diff -> retaining path -> fix -> regression guard. Если сразу "добавить больше памяти", это не решение, а временная маскировка.
Углубление (2-3 минуты)
| Сигнал | Что означает | Действие |
|---|---|---|
| Heap после GC растет | вероятный JS object leak | heap snapshot diff |
| RSS растет, heap нет | Buffer/native/addon/file handles | проверить external memory |
| GC pause растет | pressure в old space | allocation profiling |
| Listener warning | possible EventEmitter leak | проверить подписки/cleanup |
| Queue length растет | backlog удерживает объекты | лимит/дренаж/TTL |
Кейс: после каждого WebSocket reconnect память растет. Хороший ответ: проверить, удаляются ли socket listeners и entries в connection map при close, взять snapshot до/после reconnect storm, найти retaining path и добавить cleanup test.
Практика
- Составьте memory leak investigation plan: метрики, repro, snapshot 1, load, snapshot 2, comparison, retaining path, fix.
- Назовите 5 объектов-кандидатов: Map/cache, listeners, timers, queue, Buffer.
- Объясните, почему heap snapshot в production может уронить процесс.
Типичные ошибки
- Смотрят только RSS и сразу делают вывод про JS leak.
- Берут один snapshot без baseline и сравнения.
- Открывают публичный endpoint для heap snapshot.
- Лечат leak рестартом процесса без поиска retaining path.
Follow-up вопросы
- Чем heap used отличается от RSS?
- Почему snapshot может остановить main thread?
- Какой leak даст
EventEmitter memory leak warning? - Как проверить, что fix действительно убрал leak?
Что повторить
- Node heap snapshots и allocation profiler.
- V8 heap, RSS, external memory, GC.
- Retaining path, cleanup, TTL, listener lifecycle.
Связанные модули и карта
18. Когда выбирать ORM, а когда query builder?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
ORM выбирают для типового CRUD, relation mapping, migrations и type-safety. Query builder или raw SQL выбирают, когда важны точный SQL, сложные joins/window functions, performance tuning, vendor-specific features или прозрачность query plan.
Что сказать на интервью (30-60 секунд)
Я не противопоставляю ORM и SQL религиозно. Для большинства бизнес-сущностей ORM ускоряет разработку и снижает boilerplate. Но для отчетов, сложной аналитики, bulk operations и performance-critical queries я хочу видеть SQL, explain plan, индексы и контроль над транзакцией. Хороший компромисс - ORM как default, query builder/raw SQL как локальное исключение с тестами и observability.
Мини-пример
// ORM-style: удобно для простого CRUD.
const user = await db.user.findUnique({ where: { id } });
// Query-builder/raw-style: когда важен точный SQL shape.
const rows = await db.raw(sql`
SELECT account_id, sum(amount) AS total
FROM payments
WHERE created_at >= ${from}
GROUP BY account_id
`);
Пояснение
Интервьюер обычно проверяет, понимаете ли вы скрытую цену абстракции. ORM может сгенерировать лишние запросы, over-fetching, неправильный join или неожиданную транзакционную семантику. Query builder дает контроль, но переносит больше ответственности на разработчика: SQL injection safety, типы, миграции, повторное использование и тесты.
Сильный ответ всегда включает measurement: query count, duration, rows scanned, query plan, indexes, connection pool. Без измерения выбор инструмента превращается в вкусовщину.
Углубление (2-3 минуты)
| Критерий | ORM | Query builder/raw SQL |
|---|---|---|
| CRUD скорость | сильная сторона | больше boilerplate |
| Type-safety | часто встроена | зависит от tooling |
| Сложные joins/reporting | может скрывать SQL | сильная сторона |
| Performance tuning | нужен visibility | точный control |
| Vendor-specific feature | часто ограничено | доступно напрямую |
| Команда | проще онбординг | нужен SQL skill |
Кейс: endpoint отчета строится через ORM include и начинает сканировать миллионы строк. Хороший ответ: снять query plan, переписать конкретный отчет на query builder/raw SQL, добавить индексы и regression test по query count/latency, не переписывая весь data layer.
Практика
- Составьте ORM/query-builder decision matrix для трех запросов: CRUD profile, report aggregation, bulk update.
- Для каждого укажите: query count, expected rows, index, transaction boundary, test.
- Объясните, как защищаться от SQL injection при raw SQL.
Типичные ошибки
- Считают ORM бесплатной абстракцией и не смотрят SQL.
- Переходят на raw SQL без parameterization.
- Используют один giant repository method для всех query shapes.
- Не тестируют transaction boundary и query count.
Follow-up вопросы
- Как увидеть SQL, который генерирует ORM?
- Когда raw SQL оправдан в production-коде?
- Как не потерять type-safety в query builder?
- Как connection pool влияет на выбор data-access паттерна?
Что повторить
- Query plan, indexes, rows scanned.
- ORM relation loading, transactions, connection pool.
- Parameterized queries и SQL injection prevention.
Связанные модули и карта
19. Что такое N+1 на backend и как его уменьшать?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
N+1 - это паттерн, где один запрос получает список, а затем для каждого элемента выполняется дополнительный запрос. Лечат его batching, joins/eager loading, DataLoader-style aggregation, relation loading strategy и тестом на количество запросов.
Что сказать на интервью (30-60 секунд)
Я объясняю на примере: загрузили 100 пользователей одним запросом, потом в цикле запросили posts каждого пользователя - получили 101 запрос. На маленьких данных это незаметно, а на production-list превращается в latency и overload базы. Исправление зависит от контекста: join/eager loading для REST list, DataLoader для GraphQL resolvers, bulk WHERE id IN (...) для service layer.
Мини-пример
const users = await db.user.findMany();
// N+1: один запрос на users и по одному запросу на posts каждого user.
for (const user of users) {
user.posts = await db.post.findMany({ where: { authorId: user.id } });
}
Пояснение
N+1 часто появляется не в очевидном цикле, а в serializer, resolver, template или computed field. Поэтому нужно уметь измерять query count на один API call. Если endpoint возвращает 20 items, количество SQL queries не должно расти линейно от числа items без причины.
Сильный ответ не говорит "всегда join". Join может раздуть result set, eager loading может over-fetch-ить, batching может усложнить cache/invalidation. Нужно выбрать под форму данных и проверить план запроса.
Углубление (2-3 минуты)
| Сценарий | Симптом | Исправление |
|---|---|---|
| REST list + nested relation | query count растет с page size | eager loading/join |
| GraphQL nested resolvers | resolver вызывает DB на каждый parent | DataLoader/batching |
| Service loop | await внутри loop | bulk query WHERE id IN (...) |
| ORM include over-fetch | мало queries, много строк | selective fields/projection |
| Large join duplicates | huge result set | split query + batching |
Кейс: /users?include=posts быстро работает на 10 users и падает на 500. Хороший ответ: включить SQL logging/query count, увидеть 1 + N, заменить на batched posts query по authorId IN (...), сгруппировать в памяти, добавить regression test "не больше 2 SQL queries для page".
Практика
- Проведите N+1 query audit: endpoint, list size, query count, rows scanned, p95 latency.
- Перепишите пример через bulk query и группировку по foreign key.
- Сформулируйте readiness: можете доказать query count до и после.
Типичные ошибки
- Оптимизируют без измерения query count.
- Заменяют N+1 на огромный join, который возвращает слишком много строк.
- Не ставят test/guardrail на query count.
- Путают latency внешнего API с N+1 в базе.
Follow-up вопросы
- Почему GraphQL особенно часто провоцирует N+1?
- Когда batching лучше join?
- Как проверить N+1 в автоматическом тесте?
- Какие метрики базы помогут увидеть N+1?
Что повторить
- Eager loading, joins, batching, DataLoader.
- SQL logging, query count, query plan.
- Projection/selective fields и pagination.
Связанные модули и карта
20. Как обеспечивать backward compatibility API?
Теги: nodejs, backend
Сложность: Middle/Senior
Короткий ответ
Backward compatibility означает, что старые клиенты продолжают работать с новым сервером. Для API это достигается additive changes, стабильными response/error shapes, deprecation window, versioning при breaking changes и контрактными тестами.
Что сказать на интервью (30-60 секунд)
Я рассматриваю API как контракт. Обычно безопасны добавление optional response fields, новый endpoint, новый enum только если клиенты умеют unknown values. Опасны удаление/переименование поля, изменение типа, изменение default behavior, ужесточение validation и смена error shape. Перед изменением нужен consumer impact analysis, контрактные тесты, observability по использованию старого поля и понятный deprecation plan.
Мини-пример
// Compatible: добавили optional field, старые клиенты могут его игнорировать.
{
"id": "u1",
"name": "Ada",
"avatarUrl": "https://cdn.example/u1.png"
}
Пояснение
Compatibility бывает source, wire и semantic. В REST/JSON чаще всего ломают wire/semantic compatibility: клиент все еще может распарсить JSON, но поведение изменилось. Например, поле стало обязательным, список теперь возвращает меньше элементов из-за нового default pagination, enum получил новое значение, а клиент падает на exhaustive switch.
Сильный ответ включает rollout. Сначала добавить новое поле/endpoint, потом мигрировать клиентов, потом измерить usage старого контракта, объявить deprecation, и только потом удалить в новой major/versioned surface.
Углубление (2-3 минуты)
| Изменение | Обычно compatible? | Комментарий |
|---|---|---|
| Добавить optional response field | да | если клиенты игнорируют unknown fields |
| Добавить required request field | нет | старые клиенты не смогут отправить |
| Переименовать поле | нет | remove + add |
| Изменить тип поля | нет | ломает парсинг/генерированные клиенты |
| Добавить enum value | осторожно | клиенты могут не обработать unknown |
| Изменить default pagination | нет/риск | старые клиенты могут ожидать все данные |
| Изменить error envelope | нет | ломает обработку ошибок |
Кейс: API хочет заменить fullName на displayName. Хороший план: добавить displayName, оставить fullName, синхронизировать значения, обновить docs/clients, добавить telemetry использования fullName, объявить deprecation, удалить только в v2 или после согласованного окна.
Практика
- Составьте API compatibility/deprecation plan: change type, consumers, additive path, telemetry, docs, deadline, removal version.
- Для 8 изменений отметьте compatible/breaking и объясните почему.
- Опишите контрактный тест, который защищает старый client fixture.
Типичные ошибки
- Удаляют поле, потому что "клиенты уже должны были обновиться".
- Меняют enum/default/error shape без версии.
- Не знают реальных consumers и не имеют usage telemetry.
- Считают OpenAPI diff заменой контрактных тестов с реальными fixtures.
Follow-up вопросы
- Почему добавление enum value может быть breaking?
- Чем additive change отличается от semantic change?
- Когда нужна новая API version?
- Как понять, что старое поле можно удалить?
Что повторить
- Additive vs breaking API changes.
- Deprecation window, versioning, consumer telemetry.
- Contract tests, OpenAPI diff, backward-compatible rollout.
Связанные модули и карта
Куда дальше
- Вернитесь в модуль: Node.js.
- Сверьтесь с картой темы: Node.js.
- Для data-access вопросов q-18..q-19 пройдите bridge: Backend / Data Access Integration.
- Закрепите N+1 и join cardinality в SQL-песочнице: оплаченные заказы без дублей.
- Продолжайте по маршруту: Middle трек.