Перейти к основному содержимому

Node.js

Экспресс-шпаргалка 20/20

  1. Event Loop проходит фазы timers -> pending callbacks -> poll -> check -> close, а microtasks (Promise, nextTick) выполняются между фазами.
  2. Streams выбирают для больших payload и непрерывных данных, чтобы не держать все в памяти и использовать backpressure.
  3. Buffer нужен для бинарных данных: файлы, сокеты, криптография, кодировки.
  4. nextTick выполняется раньше всего, setImmediate в check, setTimeout в timers после минимальной задержки.
  5. worker_threads для CPU-bound задач, cluster для масштабирования HTTP-процесса по ядрам.
  6. CommonJS (require) проще для legacy, ESM (import) стандартнее и лучше для современного tooling.
  7. Централизованный error handling: единый error schema, middleware/filters, correlation id и разделение 4xx/5xx.
  8. Graceful shutdown: stop accepting new requests -> drain active -> close DB/broker -> forced exit by timeout.
  9. От перегрузки защищают rate limit, очереди, таймауты, circuit breaker и ограничение параллелизма.
  10. Логи должны быть структурными (JSON), с traceId/requestId, уровнями и метриками latency/error-rate.
  11. Конфиг: 12-factor, fail-fast валидация env при старте, секреты только через secret manager.
  12. Большие файлы обрабатывают stream/pipeline с лимитами размера, type-check и защитой от медленных клиентов.
  13. health показывает жив ли процесс, readiness готов ли сервис принимать трафик с учетом зависимостей.
  14. JWT безопасен только с проверкой alg, подписи, exp/aud/iss, ротацией ключей и коротким TTL access-токена.
  15. Кэш ставят на read-heavy пути с TTL, стратегией инвалидации и защитой от cache stampede.
  16. WebSocket требует heartbeat/ping-pong, лимитов соединений, backpressure и стратегии reconnect.
  17. Утечки памяти ищут через heap snapshots, allocation profiling и сравнение роста heap во времени.
  18. ORM ускоряет CRUD, query builder/raw SQL нужен для сложных и критичных по perf запросов.
  19. N+1 лечат batching, joins/eager loading и измерением количества запросов на один API call.
  20. 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 минуты)

ФазаЧто обычно выполняетсяИнтервью-ловушка
timerscallbacks setTimeout/setInterval после минимального threshold0 ms не значит "сразу"
pending callbacksнекоторые отложенные системные I/O callbacksредко нужен в прикладном коде, но фаза существует
pollприем новых I/O событий и выполнение I/O callbacksдлинный callback задержит timers
checkcallbacks setImmediateвнутри I/O обычно раньше setTimeout(..., 0)
close callbacksзакрытие handles, например socket closeважно для cleanup

Практический сценарий: API принимает большой CSV, синхронно парсит его в handler и "иногда" тормозит весь сервис. Хороший ответ: ограничить размер, читать stream-ом, тяжелую CPU-обработку вынести в worker/очередь, добавить метрику event loop lag и latency по endpoint.

Практика

  1. Выпишите порядок вывода для 3 snippets: top-level setTimeout/setImmediate, тот же код внутри fs.readFile, и код с Promise.resolve().then(...) плюс process.nextTick(...).
  2. Научитесь объяснять, почему timer может выполниться позже указанного delay.
  3. Подготовьте production-кейс: "event loop lag вырос, CPU 95%, p99 latency растет" и план диагностики.

Типичные ошибки

  1. Говорят, что Node.js "однопоточный", и забывают про libuv/system kernel для I/O и worker pool для части операций.
  2. Обещают точный порядок setTimeout(..., 0) и setImmediate без учета контекста.
  3. Не отличают I/O-bound проблему от CPU-bound проблемы.
  4. Используют рекурсивный process.nextTick и могут заstarve-ить I/O.

Follow-up вопросы

  1. Почему setTimeout(fn, 0) не гарантирует немедленное выполнение?
  2. Что произойдет с HTTP latency, если внутри handler сделать CPU-bound цикл на 300 ms?
  3. Чем event loop lag отличается от latency базы данных?
  4. Почему setImmediate внутри I/O callback обычно предсказуемее setTimeout(..., 0)?

Что повторить

  1. Фазы Event Loop и роль poll/check.
  2. Microtasks, process.nextTick, Promise.
  3. Event loop lag, CPU profiling, flamegraph.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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, логсложнее обработка ошибок
Transformgzip, CSV parser, нормализация строкнужно соблюдать backpressure
pipelineproduction-цепочка streamsнадо правильно обработать abort/cleanup

Кейс: endpoint экспортирует архив заказов. Плохой вариант - собрать весь CSV в строку и потом gzip. Хороший вариант - cursor/page source -> Transform в CSV -> gzip -> response stream, с лимитом времени, обработкой abort от клиента и метриками bytes/duration/errors.

Практика

  1. Соберите checklist: pipeline, обработка ошибок, abort клиента, лимит размера, timeout, memory RSS, slow consumer.
  2. Объясните, что делать, если .write() вернул false: остановить запись новых chunks до drain.
  3. Сравните память для readFile и stream на большом файле и сформулируйте вывод для интервью.

Типичные ошибки

  1. Используют .on('data') и вручную пишут в consumer, игнорируя write() === false.
  2. Не обрабатывают ошибки на всех участниках stream chain.
  3. Считают streams универсально быстрее: для маленького payload они могут быть лишней сложностью.
  4. Не учитывают abort клиента при streaming response.

Follow-up вопросы

  1. Почему pipeline часто лучше цепочки .pipe().pipe()?
  2. Что такое highWaterMark и как он связан с backpressure?
  3. Как защититься от клиента, который очень медленно читает response?
  4. Когда readFile все-таки нормальный выбор?

Что повторить

  1. Readable, Writable, Transform, Duplex.
  2. stream.pipeline и stream/promises.
  3. Backpressure, highWaterMark, drain.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 -> bytesencoding и Buffer.byteLengthкириллица/emoji занимают больше 1 byte
bytes -> stringправильный toString(encoding)иначе corrupted text
upload -> Bufferлимит размера до декодирования и послезащита памяти
Buffer -> streamнужен ли полный payload в памятибольшие файлы лучше stream-ить
binary protocoloffset, endianess, checksumошибка дает битый packet

Кейс: сервис принимает файл в base64. Плохой ответ - просто Buffer.from(body.file, 'base64') без лимитов. Хороший ответ - проверить максимальный размер входной строки, оценить decoded size, валидировать тип, для больших файлов использовать streaming upload/storage, а не собирать весь файл в heap.

Практика

  1. Составьте boundary map: HTTP body -> base64/string -> Buffer -> storage/stream.
  2. Покажите, почему str.length не равен размеру payload в bytes.
  3. Подготовьте ответ, когда Buffer нужен, а когда лучше stream.

Типичные ошибки

  1. Сравнивают размер строки и размер файла через length.
  2. Декодируют base64 в память без лимита.
  3. Путают Buffer с JSON object или обычным массивом чисел.
  4. Не фиксируют encoding при преобразовании string/Buffer.

Follow-up вопросы

  1. Чем Buffer.byteLength('привет') отличается от 'привет'.length?
  2. Почему Buffer.alloc безопаснее неинициализированного выделения памяти?
  3. Когда binary payload лучше вести stream-ом?
  4. Как проверить, что загруженный файл действительно нужного типа?

Что повторить

  1. Buffer.from, Buffer.alloc, Buffer.byteLength.
  2. UTF-8, base64, hex.
  3. Лимиты upload payload и stream processing.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 APIstarvation I/O
queueMicrotaskmicrotask queueпереносимый deferral для userlandошибки и порядок надо понимать
setImmediatecheck phaseотложить работу после I/O callbackне путать с timer
setTimeout(fn, 0)timers после минимального delayотложить минимум на следующий timer turnне гарантирует точное время

Кейс: библиотечная функция иногда вызывает callback синхронно, иногда после I/O. Это ломает потребителя. Решение - нормализовать callback как асинхронный, но не использовать бесконечный nextTick; для прикладной логики чаще достаточно queueMicrotask или setImmediate в зависимости от нужной очереди.

Практика

  1. Для каждого snippet предскажите вывод: top-level, внутри fs.readFile, с Promise.then, с process.nextTick.
  2. Объясните, почему рекурсивный nextTick опасен.
  3. Сформулируйте правило выбора: nextTick для Node API edge, setImmediate после I/O, setTimeout для real delay.

Типичные ошибки

  1. Называют setTimeout(fn, 0) немедленным выполнением.
  2. Считают nextTick частью обычных фаз Event Loop.
  3. Используют nextTick для тяжелой работы.
  4. Не учитывают различия CJS/ESM и microtask queue в edge cases.

Follow-up вопросы

  1. Почему process.nextTick может заstarve-ить I/O?
  2. Что будет раньше внутри I/O callback: setImmediate или setTimeout(..., 0)?
  3. Когда вы выберете queueMicrotask вместо process.nextTick?
  4. Почему timer delay - это threshold, а не SLA выполнения?

Что повторить

  1. process.nextTick queue.
  2. setImmediate и check phase.
  3. Timers API и minimum threshold.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 и дает изоляцию
Долгая фоновая задача с retryqueue + отдельный worker serviceне держит HTTP request lifecycle
I/O-heavy запросы к DB/APIобычный async I/O + limitsworker threads почти не помогут
Нужна process isolationcluster или отдельные процессыпадение изолируется лучше

Кейс: endpoint генерирует PDF 2 секунды CPU и из-за этого p99 всего API растет. Варианты: быстро вернуть job id и отправить задачу в очередь; если нужен sync-response, ограничить concurrency и вынести генерацию в worker pool. Проверка: event loop lag основного процесса падает, p99 endpoint не убивает остальные маршруты, очередь не растет бесконечно.

Практика

  1. Составьте decision matrix: CPU-bound, I/O-bound, isolation, memory sharing, deployment model.
  2. Опишите worker pool с лимитом concurrency и backpressure для входящих задач.
  3. Подготовьте ответ, почему worker_threads не лечит медленную базу данных.

Типичные ошибки

  1. Запускают worker на каждый request без pool и получают overhead/DoS по CPU.
  2. Используют workers для обычного I/O и усложняют систему без выигрыша.
  3. Путают thread-level parallelism и process-level scaling.
  4. Не продумывают graceful shutdown: что делать с задачами в worker при остановке процесса.

Follow-up вопросы

  1. Почему worker_threads полезны для CPU-bound, но не для обычного I/O-bound?
  2. Как ограничить количество задач в worker pool?
  3. Чем падение worker thread отличается от падения cluster worker process?
  4. Когда вместо cluster лучше использовать несколько container replicas?

Что повторить

  1. worker_threads: Worker, workerData, parentPort, transfer/shared memory.
  2. cluster: primary/worker processes и shared server port.
  3. CPU-bound vs I/O-bound, worker pool, queue, graceful shutdown.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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
Оставить CJSlegacy сервис, много CJS-зависимостейнет ESM-only пакетов в critical path
Перейти на ESMновый сервис, modern tooling, top-level awaittype: "module", импорты с расширениями, тесты CI
Смешанный режимпостепенная миграцияявные .cjs/.mjs boundary files
Dynamic import()CJS должен загрузить ESMобработать async boundary и ошибки загрузки

Кейс: команда обновила библиотеку, она стала ESM-only, а сервис CommonJS падает на старте. Хороший план: найти точку импорта, заменить на dynamic import() или выделить ESM boundary, проверить cold start, тесты и сборку, затем решить, нужна ли полная миграция.

Практика

  1. Составьте module interop decision table для проекта: type, entrypoint, test runner, build, CJS-зависимости, ESM-only зависимости.
  2. Напишите 2 мини-примера: ESM импортирует CJS; CJS загружает ESM через dynamic import().
  3. Объясните, почему __dirname в ESM не работает как в CJS и чем заменить.

Типичные ошибки

  1. Меняют type в package.json без проверки всех entrypoints.
  2. Думают, что import и require отличаются только синтаксисом.
  3. Ломают __dirname, __filename, JSON/native imports или test runner при миграции.
  4. Не проверяют ESM-only зависимости до обновления пакета.

Follow-up вопросы

  1. Как Node понимает, что .js файл надо читать как ESM или CJS?
  2. Что будет, если CJS попробует require ESM-модуль с top-level await?
  3. Как в ESM получить аналог __dirname?
  4. Как бы вы мигрировали большой CJS-сервис без остановки разработки?

Что повторить

  1. Node.js module resolution: .cjs, .mjs, type.
  2. ESM/CommonJS interop и dynamic import().
  3. Отличия module scope в CJS и ESM.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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Что клиентуЧто в лог
Validation400field-level code/messageinvalid fields без sensitive values
AuthN/AuthZ401/403stable error codesubject, route, policy result
Not found404resource-specific codelookup keys без секретов
Conflict409retry/change input hintversion/idempotency context
Dependency timeout502/503/504temporary failure codedependency, timeout, attempt
Unexpected bug500generic messagestack, request id, release version

Кейс: платежная интеграция иногда отвечает timeout. Плохой ответ - вернуть 500 и stack наружу. Хороший ответ - timeout классифицируется как dependency failure, клиент получает стабильный код, retry policy зависит от идемпотентности операции, в логах есть trace id и dependency latency.

Практика

  1. Составьте API error envelope mapping: domain error -> status -> public code -> log context.
  2. Разберите 5 ошибок: validation, not found, duplicate, dependency timeout, unexpected throw.
  3. Напишите правило: какие поля нельзя отдавать клиенту и какие обязательно нужны в логах.

Типичные ошибки

  1. Отдают клиенту raw err.message и stack trace.
  2. Все ошибки превращают в 500, из-за чего клиент не может правильно реагировать.
  3. Логируют без request/trace id и потом не могут связать ошибку с запросом.
  4. Забывают async/callback errors и получают unhandled rejection или зависший request.

Follow-up вопросы

  1. Чем operational error отличается от programmer error?
  2. Что делать, если ошибка произошла после отправки headers?
  3. Как сохранить backward compatibility error envelope?
  4. Какие ошибки можно retry-ить, а какие нельзя?

Что повторить

  1. Express error middleware и async handler behavior.
  2. HTTP status mapping: 400/401/403/404/409/429/5xx.
  3. Correlation id, trace id, безопасное логирование.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 drainserver.close, active requests завершаютсяp99 не обрывается массово
Resourcesзакрыть DB, cache, broker, timersпроцесс не висит из-за handles
Deadlineforced close/exit после таймаутаdeploy не зависает бесконечно

Кейс: rolling deploy обрывает платежные requests. Хороший ответ: readiness сначала, потом drain HTTP, идемпотентные операции через idempotency key, фоновые job не удаляются до ack, в логах есть shutdown phase и counts активных операций.

Практика

  1. Составьте shutdown drain checklist для HTTP + DB + queue.
  2. Опишите, что будет при активном keep-alive соединении и долгом request.
  3. Добавьте readiness-состояния: ready, draining, stopped.

Типичные ошибки

  1. Сразу вызывают process.exit(0) и режут active requests.
  2. Не ставят общий shutdown deadline.
  3. Не закрывают keep-alive/idle/upgraded connections.
  4. Забывают про фоновые timers, consumers и DB pool.

Follow-up вопросы

  1. Чем readiness отличается от liveness при shutdown?
  2. Что делать с WebSocket или HTTP upgraded connections?
  3. Как защититься от повторного SIGTERM?
  4. Как проверить graceful shutdown в e2e или staging?

Что повторить

  1. Node.js signal events.
  2. http.Server.close, closeIdleConnections, closeAllConnections.
  3. Readiness/liveness и orchestrator grace period.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 limits429, requests per key
Дорогая операцияconcurrency limit, worker pool, queuein-flight, queue depth
Медленная зависимостьtimeout, circuit breaker, fallbackdependency latency/error rate
Большой payloadbody size limit, stream, backpressureRSS memory, rejected payloads
Slowloris/медленные headersheadersTimeout, proxy timeoutopen sockets, 408
Retry stormretry budget, jitter, idempotencyretry count, 503/504

Кейс: внешний API начинает отвечать по 10 секунд, ваш сервис держит тысячи pending requests и падает по памяти. Хороший ответ: outbound timeout, concurrency limit на dependency, circuit breaker, быстрый fallback/503, метрики queue depth и saturation, плюс retry policy только для идемпотентных операций.

Практика

  1. Составьте overload control matrix: вход, route, dependency, queue, payload, socket.
  2. Для каждого контроля укажите отказ: 429, 503, timeout, fallback или enqueue.
  3. Опишите, какие метрики покажут перегрузку до падения процесса.

Типичные ошибки

  1. Ставят только rate limit и забывают dependency/concurrency limits.
  2. Делают бесконечную очередь в памяти.
  3. Ретраят все ошибки без jitter и idempotency.
  4. Не ограничивают body size и headers/request timeouts.

Follow-up вопросы

  1. Чем 429 отличается от 503 в перегрузке?
  2. Когда лучше отказать сразу, чем ставить запрос в очередь?
  3. Как timeout может усилить retry storm?
  4. Какие метрики покажут saturation Node-процесса?

Что повторить

  1. Event loop lag и "do not block the event loop".
  2. HTTP timeouts: headers/request/keep-alive.
  3. Backpressure, queue depth, concurrency limits.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 минуты)

СигналЧто хранитьЗачем
Logevent, level, requestId, route, error codeрасследовать конкретное событие
Tracetrace id, spans, parent/child, statusувидеть путь запроса по сервисам
Metriclatency, error rate, saturation, throughputувидеть тренд и алерт
Eventbusiness milestone без PIIпонять влияние на сценарий

Кейс: пользователи жалуются на sporadic 5xx. Сильный ответ: по error rate алертим, по trace id находим route, в span видим external call timeout, в логах есть public error code и dependency attempt, в метриках подтверждаем рост p99 latency именно у dependency.

Практика

  1. Составьте logging/tracing signal map для одного endpoint: request start, DB call, external call, response, error.
  2. Укажите, какие поля обязательны, какие запрещены, какие имеют high-cardinality риск.
  3. Подготовьте 60-секундный incident walkthrough от alert до root cause.

Типичные ошибки

  1. Пишут plain text logs без request id и structured fields.
  2. Логируют raw body, токены, cookies или персональные данные.
  3. Путают logs, metrics и traces и пытаются одним сигналом заменить все.
  4. Добавляют high-cardinality labels в метрики, например user id.

Follow-up вопросы

  1. Чем log отличается от trace span?
  2. Какие поля нужны в error log для расследования?
  3. Почему user id опасен как metric label?
  4. Как проверить, что trace id не теряется в async code?

Что повторить

  1. OpenTelemetry traces, spans, context propagation.
  2. Structured logging и redaction.
  3. RED/USE style metrics: rate, errors, duration, saturation.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 envDATABASE_URLесть, валидный URL
NumberPORT, TIMEOUT_MSinteger, диапазон
EnumNODE_ENV, LOG_LEVELтолько разрешенные значения
BooleanFEATURE_X_ENABLEDявный parser, не truthy string
SecretJWT_PRIVATE_KEYне логируется, доступ ограничен, есть ротация

Кейс: CACHE_TTL_SECONDS="0" случайно трактуется как truthy string и включает кэш без TTL. Сильный ответ: config schema парсит number, проверяет min/max, падает на старте и имеет тест на production env sample.

Практика

  1. Составьте env validation checklist: required, type, range, default, secret/redaction, owner.
  2. Объясните, почему process.env.FEATURE_ENABLED === 'false' опасно проверять как boolean.
  3. Подготовьте runbook ротации секрета: новый ключ, dual-read/dual-sign, rollout, revoke old.

Типичные ошибки

  1. Читают process.env прямо в бизнес-логике.
  2. Не валидируют env при старте и получают runtime-инцидент.
  3. Логируют secrets в error context или config dump.
  4. Путают public config и secret config.

Follow-up вопросы

  1. Почему process.env недостаточно как config layer?
  2. Как безопасно ротировать JWT signing key?
  3. Какие env значения нельзя иметь с default?
  4. Что должно быть в .env.example, а чего там быть не должно?

Что повторить

  1. Node.js process.env и env files.
  2. 12-factor config principle.
  3. Secret redaction, rotation, least privilege.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 limitbytes до/во время чтенияOOM, переполнение диска
Type/signatureMIME и magic bytesвредный или битый файл
Timeout/rateслишком медленный клиентзанятые sockets
Backpressureскорость consumerрост памяти
Cleanuptemp file/objectмусор и cost leak
Async processingтяжелая обработкаблокировка request path

Кейс: пользователи грузят видео, а сервис сначала собирает весь файл в память и потом отправляет в storage. Под нагрузкой RSS растет и процесс падает. Хороший ответ: streaming upload напрямую в storage/temp file, лимит размера, abort cleanup, очередь на transcode, отдельные метрики upload bytes/duration/errors.

Практика

  1. Составьте large-upload pipeline guard list: auth, size, type, timeout, storage, cleanup, async job.
  2. Объясните, что делать при aborted request.
  3. Назовите метрики: active uploads, bytes uploaded, duration, rejected by size/type, storage error rate.

Типичные ошибки

  1. Используют in-memory upload для больших файлов.
  2. Проверяют только Content-Type, но не signature/размер.
  3. Не удаляют temp file при abort/error.
  4. Делают CPU-heavy обработку прямо в HTTP handler.

Follow-up вопросы

  1. Чем multipart upload отличается от JSON base64 upload по рискам памяти?
  2. Как защититься от slow upload клиента?
  3. Почему pipeline лучше ручной связки listeners?
  4. Когда нужен pre-signed upload напрямую в object storage?

Что повторить

  1. stream.pipeline и backpressure.
  2. HTTP request timeouts и abort handling.
  3. File type validation, temp file cleanup, object storage flow.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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.

Практика

  1. Составьте health/readiness dependency matrix: DB, cache, broker, external API, local disk.
  2. Для каждой зависимости решите: влияет на readiness или только на метрики.
  3. Опишите поведение endpoint во время startup, normal, dependency degraded, draining.

Типичные ошибки

  1. Один endpoint одновременно для liveness, readiness и diagnostics.
  2. Healthcheck делает дорогие запросы без timeout.
  3. Liveness зависит от внешней базы и вызывает restart storm.
  4. При shutdown readiness остается 200, и traffic продолжает идти на draining instance.

Follow-up вопросы

  1. Почему readiness должна стать false до server.close?
  2. Какие зависимости нельзя проверять в liveness?
  3. Как избежать thundering herd от health checks?
  4. Чем /metrics отличается от /ready?

Что повторить

  1. Liveness, readiness, startup probes.
  2. Shutdown draining lifecycle.
  3. Dependency timeout, cached health status, alerting by metrics.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 для критичных событий.

Практика

  1. Составьте JWT verification checklist: token source, alg, key, signature, exp, iss, aud, subject, scopes.
  2. Объясните, почему jwt.decode не равен jwt.verify.
  3. Спроектируйте key rotation: kid, JWKS cache TTL, old/new keys overlap, forced revoke.

Типичные ошибки

  1. Доверяют decoded payload без signature verification.
  2. Не проверяют aud/iss и принимают чужой токен.
  3. Хранят секреты или PII внутри JWT payload.
  4. Путают role claim с полной authorization policy.

Follow-up вопросы

  1. Чем access token отличается от refresh token?
  2. Как отозвать JWT до exp?
  3. Что такое kid и зачем нужен JWKS cache?
  4. Почему JWT payload нельзя считать приватным?

Что повторить

  1. JWT structure: header, payload, signature.
  2. iss, aud, exp, nbf, sub, kid.
  3. Access/refresh tokens, revoke, rotation, authn vs authz.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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-asideread-heavy данные между instancesstale 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.

Практика

  1. Составьте cache invalidation/stampede matrix: key, TTL, invalidation trigger, stale tolerance, stampede guard.
  2. Разберите 3 типа данных: профиль пользователя, каталог, permissions.
  3. Назовите метрики: hit rate, miss rate, stale served, set/get errors, eviction, lock wait.

Типичные ошибки

  1. Не определяют источник истины и допустимую stale-ness.
  2. Делают key без version/tenant/user scope и смешивают данные.
  3. Не защищаются от cache stampede.
  4. Считают внешний кэш always available и не проектируют fallback.

Follow-up вопросы

  1. Чем cache-aside отличается от read-through?
  2. Когда in-memory cache опасен в multi-instance сервисе?
  3. Как инвалидировать кэш после write?
  4. Что делать при падении Redis: fail open или fail closed?

Что повторить

  1. Cache-aside, TTL, invalidation.
  2. Stampede protection: lock, singleflight, stale-while-revalidate, jitter.
  3. Cache key design, serialization, multi-instance consistency.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 heartbeatstale sockets закрываются
Slow clientлимит buffered messagesпамять не растет без лимита
Fan-out overloadrate limit, batching, pub/subbroadcast не блокирует Event Loop
Потеря сообщенийmessage id, ack, resumereconnect не ломает сценарий
Multi-instancesticky session или shared pub/subroom state не только в памяти
Deploy/shutdowndrain, close code, reconnect hintклиенты переподключаются корректно

Кейс: чат работает на одном instance, потом его масштабировали до трех процессов. Пользователи в разных процессах перестали видеть сообщения друг друга. Хороший ответ: вынести room membership/pub-sub state во внешний слой, решить sticky sessions, добавить message id/ack и метрики active connections/fan-out lag.

Практика

  1. Составьте WebSocket reliability checklist: auth, heartbeat, limits, backpressure, reconnect, ordering, shutdown.
  2. Опишите reconnect flow: last seen message id, resume, missed messages, duplicate handling.
  3. Назовите метрики: active connections, messages/sec, buffered messages, reconnect rate, close codes.

Типичные ошибки

  1. Хранят весь state rooms только в памяти одного процесса.
  2. Не закрывают мертвые соединения и получают утечку sockets.
  3. Не ограничивают slow clients и buffered messages.
  4. Считают WebSocket заменой всем REST endpoints.

Follow-up вопросы

  1. Когда выбрать SSE вместо WebSocket?
  2. Как масштабировать rooms между несколькими Node instances?
  3. Что делать, если клиент пропустил сообщения во время reconnect?
  4. Как graceful shutdown влияет на WebSocket connections?

Что повторить

  1. WebSocket handshake, close codes, ping/pong.
  2. Backpressure/slow clients и message buffering.
  3. Pub/sub, sticky sessions, reconnect/resume.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 leakheap snapshot diff
RSS растет, heap нетBuffer/native/addon/file handlesпроверить external memory
GC pause растетpressure в old spaceallocation profiling
Listener warningpossible EventEmitter leakпроверить подписки/cleanup
Queue length растетbacklog удерживает объектылимит/дренаж/TTL

Кейс: после каждого WebSocket reconnect память растет. Хороший ответ: проверить, удаляются ли socket listeners и entries в connection map при close, взять snapshot до/после reconnect storm, найти retaining path и добавить cleanup test.

Практика

  1. Составьте memory leak investigation plan: метрики, repro, snapshot 1, load, snapshot 2, comparison, retaining path, fix.
  2. Назовите 5 объектов-кандидатов: Map/cache, listeners, timers, queue, Buffer.
  3. Объясните, почему heap snapshot в production может уронить процесс.

Типичные ошибки

  1. Смотрят только RSS и сразу делают вывод про JS leak.
  2. Берут один snapshot без baseline и сравнения.
  3. Открывают публичный endpoint для heap snapshot.
  4. Лечат leak рестартом процесса без поиска retaining path.

Follow-up вопросы

  1. Чем heap used отличается от RSS?
  2. Почему snapshot может остановить main thread?
  3. Какой leak даст EventEmitter memory leak warning?
  4. Как проверить, что fix действительно убрал leak?

Что повторить

  1. Node heap snapshots и allocation profiler.
  2. V8 heap, RSS, external memory, GC.
  3. Retaining path, cleanup, TTL, listener lifecycle.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 минуты)

КритерийORMQuery 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.

Практика

  1. Составьте ORM/query-builder decision matrix для трех запросов: CRUD profile, report aggregation, bulk update.
  2. Для каждого укажите: query count, expected rows, index, transaction boundary, test.
  3. Объясните, как защищаться от SQL injection при raw SQL.

Типичные ошибки

  1. Считают ORM бесплатной абстракцией и не смотрят SQL.
  2. Переходят на raw SQL без parameterization.
  3. Используют один giant repository method для всех query shapes.
  4. Не тестируют transaction boundary и query count.

Follow-up вопросы

  1. Как увидеть SQL, который генерирует ORM?
  2. Когда raw SQL оправдан в production-коде?
  3. Как не потерять type-safety в query builder?
  4. Как connection pool влияет на выбор data-access паттерна?

Что повторить

  1. Query plan, indexes, rows scanned.
  2. ORM relation loading, transactions, connection pool.
  3. Parameterized queries и SQL injection prevention.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 relationquery count растет с page sizeeager loading/join
GraphQL nested resolversresolver вызывает DB на каждый parentDataLoader/batching
Service loopawait внутри loopbulk query WHERE id IN (...)
ORM include over-fetchмало queries, много строкselective fields/projection
Large join duplicateshuge result setsplit 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".

Практика

  1. Проведите N+1 query audit: endpoint, list size, query count, rows scanned, p95 latency.
  2. Перепишите пример через bulk query и группировку по foreign key.
  3. Сформулируйте readiness: можете доказать query count до и после.

Типичные ошибки

  1. Оптимизируют без измерения query count.
  2. Заменяют N+1 на огромный join, который возвращает слишком много строк.
  3. Не ставят test/guardrail на query count.
  4. Путают latency внешнего API с N+1 в базе.

Follow-up вопросы

  1. Почему GraphQL особенно часто провоцирует N+1?
  2. Когда batching лучше join?
  3. Как проверить N+1 в автоматическом тесте?
  4. Какие метрики базы помогут увидеть N+1?

Что повторить

  1. Eager loading, joins, batching, DataLoader.
  2. SQL logging, query count, query plan.
  3. Projection/selective fields и pagination.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

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 или после согласованного окна.

Практика

  1. Составьте API compatibility/deprecation plan: change type, consumers, additive path, telemetry, docs, deadline, removal version.
  2. Для 8 изменений отметьте compatible/breaking и объясните почему.
  3. Опишите контрактный тест, который защищает старый client fixture.

Типичные ошибки

  1. Удаляют поле, потому что "клиенты уже должны были обновиться".
  2. Меняют enum/default/error shape без версии.
  3. Не знают реальных consumers и не имеют usage telemetry.
  4. Считают OpenAPI diff заменой контрактных тестов с реальными fixtures.

Follow-up вопросы

  1. Почему добавление enum value может быть breaking?
  2. Чем additive change отличается от semantic change?
  3. Когда нужна новая API version?
  4. Как понять, что старое поле можно удалить?

Что повторить

  1. Additive vs breaking API changes.
  2. Deprecation window, versioning, consumer telemetry.
  3. Contract tests, OpenAPI diff, backward-compatible rollout.

Связанные модули и карта

  1. Обучение: Node.js
  2. Обучение: Web и сеть
  3. Карта подготовки: Node.js

Куда дальше

  1. Вернитесь в модуль: Node.js.
  2. Сверьтесь с картой темы: Node.js.
  3. Для data-access вопросов q-18..q-19 пройдите bridge: Backend / Data Access Integration.
  4. Закрепите N+1 и join cardinality в SQL-песочнице: оплаченные заказы без дублей.
  5. Продолжайте по маршруту: Middle трек.