Вопросы на собеседовании по Node.js

Собрал вопросы, которые чаще всего задают backend-разработчикам на Node.js на собесах уровня junior и middle. К каждому даю короткий ответ, с которым не стыдно выйти к интервьюеру, и подсказываю, где обычно сыпятся

Дима — backend-менторДимаSenior Backend, провожу собеседования

21 вопрос7 тем~10 мин чтения

Тема 015 вопросов

Event loop и асинхронность

01Как устроен event loop в Node.js?

Event loop живёт в библиотеке libuv и проходит по кругу несколько фаз. Timers выполняет колбэки setTimeout и setInterval. Poll ждёт и обрабатывает I/O. Check выполняет setImmediate. Есть ещё служебные фазы: pending callbacks, idle/prepare и close callbacks

В старых статьях timers стоит первой фазой. С Node.js 20 (libuv 1.45) таймеры в каждой итерации проверяются после poll, а до входа в цикл запускаются один раз. Если расскажешь про это, интервьюер точно оценит

После каждого колбэка Node выполняет микрозадачи. В CommonJS сначала идёт очередь process.nextTick, потом промисы. Пока микрозадачи не закончатся, цикл дальше не двинется, поэтому рекурсивный nextTick может вообще не пустить I/O

02В каком порядке выполнится код с setTimeout, setImmediate, nextTick и Promise?

Сначала выполнится весь синхронный код, потом микрозадачи. В CommonJS process.nextTick идёт раньше промисов. В ES-модуле наоборот: сам модуль выполняется как микрозадача, и promise напечатается раньше nextTick

А вот кто раньше, setTimeout(…, 0) или setImmediate, в главном модуле не гарантировано. Это зависит от того, успел ли истечь таймер к старту цикла. Внутри I/O-колбэка setImmediate всегда сработает первым, потому что фаза check идёт сразу после poll

JavaScript
console.log('1');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('2');

// CommonJS: 1, 2, nextTick, promise
// ES-модуль: 1, 2, promise, nextTick
// потом timeout и immediate, их порядок не гарантирован

03Node.js однопоточный? Что такое thread pool libuv?

Однопоточный только JavaScript, сам процесс использует несколько потоков. Сетевой I/O libuv делает неблокирующим средствами ОС (epoll, kqueue, IOCP). А то, что ОС не умеет делать асинхронно, уходит в пул потоков libuv: файловая система, dns.lookup, crypto.pbkdf2, zlib

По умолчанию в пуле 4 потока. Размер меняют переменной окружения UV_THREADPOOL_SIZE, максимум 1024, и задают её при старте процесса. Если запустить много тяжёлых операций crypto одновременно, они встанут в очередь как раз в этом пуле

04Что делать с CPU-bound задачами в Node.js?

Тяжёлые вычисления в основном потоке блокируют event loop, и сервер перестаёт отвечать всем сразу. Варианты такие: worker_threads для вычислений внутри процесса, отдельный сервис или очередь задач (BullMQ, Kafka). Чтобы просто занять все ядра, запускают несколько процессов через cluster, PM2 или реплики в Kubernetes

Задержку event loop можно мерить через perf_hooks.monitorEventLoopDelay и выводить в метрики

05Чем отличаются Promise.all, allSettled, race и any?

all ждёт все промисы и падает на первой ошибке. allSettled ждёт все и возвращает статус каждого. race завершается вместе с первым промисом, неважно, успешным или нет. any ждёт первый успешный, а если упадут все, бросает AggregateError

Тут любят подловить: all при ошибке не отменяет остальные операции, они продолжают работать. Чтобы их правда остановить, нужен AbortController

Тема 022 вопроса

Стримы и буферы

06Какие бывают стримы и что такое backpressure?

Стримы бывают четырёх видов: Readable, Writable, Duplex (например, сокет) и Transform (сжатие, парсинг). С ними можно обработать файл или ответ любого размера и не держать его целиком в памяти

Backpressure притормаживает быстрый источник, когда приёмник не успевает. write() возвращает false, и писать дальше стоит только после события drain. Проще всего соединять стримы через pipeline из node:stream/promises: он сам следит за backpressure, пробрасывает ошибки и закрывает все стримы

JavaScript
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';

await pipeline(
  createReadStream('dump.sql'),
  createGzip(),
  createWriteStream('dump.sql.gz'),
);

07Что такое Buffer?

Buffer это массив байт фиксированной длины, наследник Uint8Array. Его память лежит вне кучи JS-объектов V8, а маленькие буферы меньше 4 КБ из Buffer.from и allocUnsafe нарезаются из общего пула. Через буферы идут бинарные данные: файлы, сеть, криптография

Buffer.alloc заполняет память нулями, а Buffer.allocUnsafe нет. В такой буфер могут попасть старые данные процесса, поэтому его берут, только когда сразу перезаписывают целиком

Мок-собеседование

Знать ответы мало, надо уверенно отвечать вслух

На менторстве проводим мок-собеседования по каждому блоку и доводим до оффера на Node.js. Основную часть оплаты вносишь после трудоустройства

Тема 033 вопроса

Память и производительность

08Как устроен сборщик мусора V8?

Сборщик в V8 работает с поколениями. Новые объекты попадают в young generation, её быстро чистит Scavenge: он копирует живые объекты между двумя половинами памяти. Объекты, которые пережили две такие сборки, переезжают в old generation. Её обрабатывает Mark-Sweep-Compact, и большая часть этой работы идёт по частям и в фоновых потоках

Лимит old space задают флагом --max-old-space-size. В контейнере его стоит согласовать с лимитом памяти пода

09Откуда берутся утечки памяти и как их искать?

Чаще всего память течёт из-за кэша без лимита и TTL, слушателей событий, которые добавили и забыли снять (Node предупредит MaxListenersExceededWarning), незакрытых таймеров и замыканий, которые держат большие объекты

Утечку видно по росту heapUsed в метриках. Дальше снимаешь два снимка кучи через --inspect и Chrome DevTools или через v8.writeHeapSnapshot(), сравниваешь их и смотришь, какие объекты копятся

10Чем cluster отличается от worker_threads?

cluster запускает несколько процессов, у каждого своя память и свой V8, а порт у них общий. Так HTTP-сервер занимает все ядра. worker_threads это потоки внутри одного процесса. У каждого тоже свой event loop и изолят V8, но данные можно передавать через postMessage, а память делить через SharedArrayBuffer

В Kubernetes обычно запускают один процесс на под и масштабируются репликами, а worker_threads оставляют для тяжёлых вычислений

Тема 042 вопроса

Модули и ошибки

11Чем CommonJS отличается от ES-модулей?

CommonJS (require, module.exports) загружает модули синхронно, прямо во время выполнения, а экспорт там обычный объект. ES-модули (import, export) разбираются статически ещё до запуска. Отсюда tree shaking, top-level await и живые привязки экспортов

Какой формат у файла, решает поле "type" в package.json или расширение .mjs и .cjs. С Node.js 20.19 и 22.12 require() без флагов умеет загружать ES-модули, если в них нет top-level await

12Как обрабатывать ошибки в асинхронном коде?

В async-функциях ошибки ловят через try/catch вокруг await. Если промис отклонился и его никто не обработал, с Node.js 15 процесс по умолчанию падает (unhandledRejection). И это правильно: процесс в непонятном состоянии лучше перезапустить

В Express 4 ошибку из async-обработчика надо самому передать в next(err), а Express 5 и Nest.js ловят отклонённые промисы сами. process.on("uncaughtException") нужен только чтобы залогировать ошибку и аккуратно завершиться, а не чтобы работать дальше

Тема 052 вопроса

Nest.js

13Как устроен DI в Nest.js и какие бывают scope у провайдеров?

Приложение на Nest собирается из модулей. Модуль объявляет провайдеры, экспортирует их и импортирует чужие. IoC-контейнер сам создаёт экземпляры и передаёт зависимости через конструктор. Зависимость ищется по токену: это класс, строка или Symbol вместе с @Inject

По умолчанию провайдер один на всё приложение. Со scope REQUEST экземпляр создаётся на каждый запрос, и такими же становятся все провайдеры, которые от него зависят. Это бьёт по производительности. Со scope TRANSIENT каждый потребитель получает свой экземпляр

14В каком порядке выполняются middleware, guards, interceptors, pipes и filters?

Запрос сначала проходит middleware, потом guards. Дальше срабатывает часть interceptors до next.handle(), потом pipes, и только затем сам обработчик. После него выполняется вторая часть interceptors. Если где-то вылетела ошибка, её ловят exception filters

Guards решают, можно ли пустить запрос, на них обычно авторизация. Pipes валидируют и преобразуют входные данные. Interceptors оборачивают обработчик: логирование, кэш, преобразование ответа, таймауты

Тема 062 вопроса

TypeScript

15Чем type отличается от interface?

interface описывает форму объекта. Его можно расширять через extends, а два объявления с одним именем сливаются в одно (declaration merging), так дополняют типы библиотек. type даёт имя любому типу: объединению, пересечению, кортежу, условному или mapped-типу

Я обычно беру interface для публичных контрактов объектов, а type для всего остального

16Чем unknown отличается от any и зачем нужен never?

any просто выключает проверку типов. В unknown тоже можно положить что угодно, но работать с этим значением нельзя, пока не сузишь тип проверкой. Поэтому для внешних данных правильнее брать unknown и валидировать, например через zod

never означает значение, которого не может быть. Он нужен для исчерпывающих проверок. Если в union добавят новый вариант и забудут обработать его в switch, присваивание в never в ветке default не скомпилируется

TypeScript
type Status = 'new' | 'paid' | 'shipped';

function label(s: Status): string {
  switch (s) {
    case 'new': return 'Новый';
    case 'paid': return 'Оплачен';
    case 'shipped': return 'Отправлен';
    default: {
      const unreachable: never = s; // ошибка, если забыли вариант
      return unreachable;
    }
  }
}

Тема 075 вопросов

Базы данных и микросервисы

17Что такое проблема N+1 и как её решить?

Это когда ты одним запросом получаешь список, а потом для каждого элемента делаешь ещё по запросу за связанными данными. На 100 заказах выходит 101 запрос. В ORM так бывает при ленивой загрузке связей (TypeORM, Sequelize) или когда связи достают вручную в цикле

Лечится через JOIN или явную подгрузку связей (relations, include). Ещё можно одним запросом с WHERE id = ANY($1) забрать всё сразу и сгруппировать в коде. В GraphQL для этого есть DataLoader

18Какие уровни изоляции есть в PostgreSQL и какой выбрать?

В PostgreSQL есть Read Committed (он по умолчанию), Repeatable Read и Serializable. Read Uncommitted там работает так же, как Read Committed. Repeatable Read работает со снимком данных, но пропускает аномалию write skew, а Serializable закрывает и её. На обоих этих уровнях Postgres может откатить транзакцию с ошибкой сериализации (код 40001), так что приложение должно уметь её повторить

Для типичной задачи вроде списания баланса обычно хватает Read Committed с SELECT … FOR UPDATE или одного атомарного UPDATE … SET balance = balance - $1 WHERE balance >= $1

19Как сделать обработку сообщений из Kafka идемпотентной?

В Kafka есть встроенный exactly-once на идемпотентном продюсере и транзакциях, но он работает, только когда и читаешь, и пишешь в Kafka. Порядок сообщений гарантирован лишь внутри партиции. Если результат пишется во внешнюю базу, на деле получается at-least-once: после ребалансировки или падения консьюмера сообщение может прийти ещё раз. Поэтому обработчик делают идемпотентным, например хранят ключи обработанных сообщений или ставят уникальный индекс в базе

Если нужно и изменить данные, и отправить событие, используют transactional outbox. Событие пишут в отдельную таблицу в той же транзакции, а отдельный воркер потом отправляет его в брокер

20Как устроена аутентификация на JWT с refresh-токенами?

Access-токен живёт несколько минут, и сервис проверяет его подпись без похода в базу. Refresh-токен живёт долго, на сервере хранят его или его хеш, и по нему выдают новую пару токенов. При каждом обновлении refresh-токен меняют, а если кто-то пришёл со старым, это считают кражей и закрывают всю сессию

В браузере refresh-токен кладут в cookie. Флаг httpOnly не даёт прочитать его из JavaScript при XSS, Secure пускает его только по HTTPS, а SameSite защищает от CSRF. Ещё полезно ограничить Path адресом, где обновляют токен

21Как корректно остановить Node.js-сервис?

Ловим SIGTERM и SIGINT и вызываем server.close(), чтобы перестать принимать новые соединения. Учти, что он ждёт закрытия соединений, а не завершения запросов. С Node.js 19 простаивающие keep-alive соединения он закрывает сам, а оставшиеся после таймаута можно закрыть через server.closeAllConnections(). Потом закрываем консьюмеры очередей, пул БД и таймеры. На всё ставим общий таймаут, после которого процесс завершается принудительно

В Kubernetes перед остановкой стоит подождать несколько секунд или добавить preStop-хук. Под убирают из Service не мгновенно, и какое-то время на него ещё приходит трафик

В Nest.js для этого есть app.enableShutdownHooks() и хуки onModuleDestroy и beforeApplicationShutdown

Это только часть вопросов

Полная подготовка к собеседованию на менторстве

Здесь собраны самые частые вопросы. На менторстве по Node.js у тебя будет вся база и личная подготовка до оффера

  • Детальная база знанийТемы собеседования по Node.js с разбором, примерами и уточняющими вопросами интервьюеров
  • Roadmap из 5 блоковПлан подготовки под твой уровень и цель, а после каждого блока мок-собеседование
  • 350+ реальных собеседованийЗаписи собесов из реальных компаний: что спрашивают и как отвечают те, кто получил оффер

Готовишься на другой стек? Вопросы на собеседовании Golang