Node.js — среда выполнения JavaScript вне браузера, построенная на V8 и расширенная API для сети, файлов, процессов, потоков и ОС. Его используют для REST API, GraphQL, WebSocket-серверов, микросервисов, CLI, proxy и API Gateway. Сильная сторона — неблокирующий I/O: один процесс эффективно обслуживает много соединений. Компромисс в том, что длительные CPU-heavy вычисления могут заблокировать event loop, если не вынести их в workers, processes или отдельный сервис.
2Как Node.js работает внутри?
JavaScript выполняется в V8, а Node.js API передают асинхронную работу операционной системе или thread pool libuv. Главный JavaScript-поток не ждет завершения I/O и продолжает выполнять другой код. Когда операция завершается, ее callback или Promise continuation становится доступным event loop. Поэтому Node.js достигает высокой конкурентности, хотя пользовательский JavaScript обычно выполняется в одном главном потоке.
3Что такое V8, libuv и event loop?
V8 компилирует и выполняет JavaScript, управляет объектами и памятью. libuv предоставляет кроссплатформенный event loop, интеграцию async I/O и worker thread pool для части filesystem, DNS и crypto операций. Event loop решает, когда запускать callbacks завершенных операций. Node.js связывает JavaScript API с native capabilities; сам V8 не дает HTTP-сервер, filesystem или process API.
4Почему Node.js называют однопоточным?
Один Node.js-процесс обычно выполняет пользовательский JavaScript в одном главном потоке, поэтому два обычных request handler не могут выполнять JavaScript одновременно в одном и том же потоке. Но платформа не буквально однопоточная: есть thread pool libuv, async I/O ОС, garbage collection V8, worker_threads и child processes. Фраза относится прежде всего к default модели исполнения JavaScript.
5Как Node.js обрабатывает много запросов одновременно?
Node.js запускает I/O операции и возвращается в event loop вместо того, чтобы блокировать поток на каждый запрос. Например, 1000 запросов к базе могут быть in flight, пока главный поток принимает соединения и обрабатывает готовые callbacks. Это хорошо для I/O-bound workloads: HTTP, база, файлы, sockets. Но тяжелый синхронный CPU-код в handler блокирует event loop и задерживает все остальные запросы.
6Какие основные фазы есть у event loop?
В libuv есть phases: timers, pending callbacks, idle/prepare, poll, check и close callbacks. Timers запускает setTimeout/setInterval, poll обрабатывает I/O, check запускает setImmediate, close обрабатывает закрытия sockets. Отдельно существуют microtask queues: process.nextTick и Promise microtasks. Если микрозадач слишком много, они могут не дать event loop своевременно перейти к I/O.
7Чем отличаются синхронные, асинхронные и неблокирующие операции?
Синхронная операция возвращает результат до перехода к следующей строке и блокирует главный поток, например fs.readFileSync в request handler. Асинхронная операция возвращает результат позже через callback или Promise. Неблокирующая модель означает, что поток может делать другую работу, пока операция не завершилась. В server handlers обычно выбирают async non-blocking API.
Блокировка цикла событий или event loop starvation возникает, когда JavaScript-код или microtasks слишком долго занимают event loop и он не успевает обрабатывать I/O, timers и новые запросы. Причины: тяжелые синхронные вычисления, огромный JSON processing, бесконечные Promise chains, чрезмерный process.nextTick. Решения: разбивать работу, ограничивать входные данные, профилировать CPU, использовать worker_threads или отдельный сервис и избегать sync API на горячем пути.
Callback — функция, которую передают для последующего выполнения, часто после завершения async операции. Классический Node.js стиль — error-first callback: callback(error, result). Callback hell появляется при глубокой вложенности зависимых операций, из-за чего сложнее читать flow и обрабатывать ошибки. Помогают named functions, Promises, async/await и Promise.all для независимых операций.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
10Что такое Promise и как работает async/await?
Promise представляет будущий результат async операции и имеет состояния pending, fulfilled или rejected. async-функция всегда возвращает Promise. await приостанавливает только текущую async-функцию, но не весь Node.js процесс. В хорошем ответе важно упомянуть try/catch, обработку rejected Promises, отсутствие висящих или необработанных Promise и Promise.all для независимой работы.
11Чем отличаются process.nextTick, queueMicrotask, setImmediate и setTimeout?
process.nextTick выполняется сразу после текущей операции до перехода event loop дальше. queueMicrotask добавляет стандартную JavaScript microtask, похожую на Promise continuation. setImmediate выполняется в check phase. setTimeout(fn, 0) выполняется в timers phase после минимальной задержки. Обычно nextTick идет раньше Promise microtasks, а порядок setTimeout(0) и setImmediate зависит от контекста; после I/O callback setImmediate часто раньше.
Синхронный код оборачивают в try/catch. Promise-based код обрабатывают через await в try/catch или .catch. Error-first callbacks сначала проверяют error. В серверных приложениях нужны central error middleware, request-context logging, безопасные сообщения клиенту и graceful shutdown. uncaughtException не стоит использовать для продолжения нормальной работы поврежденного процесса.
Модуль — изолированная единица кода с явно экспортируемым интерфейсом. В Node.js есть built-in modules вроде node:fs и node:http, локальные модули проекта, npm packages, CommonJS и ECMAScript modules. Модули помогают разделять ответственность и переиспользовать код. Production-компромисс — coupling: глубокие циклические импорты и side effects при import усложняют тестирование и startup behavior.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Чем CommonJS require отличается от ES Modules import?
CommonJS использует require и module.exports, ES Modules используют import и export. ESM поддерживает static analysis и top-level await; режим включается через package.json type: module или .mjs. CommonJS часто загружается синхронно и может обозначаться .cjs. В современных проектах встречаются оба формата, поэтому важно учитывать file extension, package.json и контекст проекта.
15Что такое npm, package.json и package-lock.json?
npm — package manager и CLI. package.json описывает scripts, metadata, dependencies и devDependencies. package-lock.json фиксирует точное дерево зависимостей, чтобы установка была воспроизводимой. В CI обычно используют npm ci: он ставит строго по lock file и падает, если package.json и lock не согласованы. Это защищает build от случайного drift зависимостей.
16Как управлять конфигурацией и переменными окружения?
Node.js читает env vars через process.env, но значения являются строками, поэтому их нужно валидировать и приводить к типам при startup. Secrets нельзя хранить в репозитории, .env должен быть в .gitignore, production secrets должны приходить из платформы или secret manager. Сервис должен fail fast, если обязательная config отсутствует. NODE_ENV — соглашение, а не защищенный security boundary.
const port = Number(process.env.PORT ?? 3000);
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('DATABASE_URL is required');
}
Events, Buffer и streams
17Что такое EventEmitter?
EventEmitter реализует publish-subscribe внутри Node.js процесса. on регистрирует listener, once — listener на одно событие, off удаляет listener, emit синхронно вызывает listeners в порядке регистрации. Синхронность важна: медленный listener блокирует emitter. Событие error особое: необработанный error event может завершить процесс. В долгоживущих системах нужно удалять ненужные listeners, чтобы не создавать memory leaks.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
18Для чего нужен Buffer?
Buffer представляет фиксированную последовательность байтов. Он нужен для файлов, TCP sockets, HTTP bodies, images, crypto и binary protocols. Buffer.from создает buffer из данных, Buffer.alloc выделяет zero-filled память, Buffer.allocUnsafe быстрее, но может содержать остаточные данные из памяти до перезаписи. allocUnsafe безопасен только если код сразу полностью заполняет buffer и не раскрывает stale data.
Streams обрабатывают данные частями, не загружая все в память. Основные виды: Readable, Writable, Duplex и Transform. Примеры: файлы, HTTP request/response, TCP sockets, gzip streams. Streams полезны для больших файлов и сетевого трафика, потому что снижают memory usage и latency. В production важно корректно обрабатывать errors по всей цепочке.
Backpressure возникает, когда источник генерирует chunks быстрее, чем получатель может их обработать. Без backpressure память может расти до замедления или падения процесса. Streams и pipe умеют координировать flow, но pipeline из node:stream/promises безопаснее для сложных цепочек: он пробрасывает errors, закрывает связанные streams и возвращает Promise. Это хороший default для compression, uploads, downloads и transformations.
import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);
Другие вопросы для backend интервью
Переходите между технологиями или вернитесь в общий hub подготовки.