Питання та відповіді для співбесіди з Node.js 2026
Цілеспрямована добірка питань для співбесіди з Node.js для Backend-розробників. Відповіді структуровані для усної практики: поясніть архітектуру, наведіть приклад і згадайте про компроміси в продакшені.
Node.js — це середовище виконання JavaScript поза браузером, побудоване на V8 і розширене API (Application Programming Interface) для роботи з мережею, файлами, процесами, потоками даних (streams) і доступом до операційної системи. Його зазвичай використовують для REST API, сервісів GraphQL, серверів WebSocket у реальному часі, мікросервісів, інструментів командного рядка (CLI), проксі та API-шлюзів. Головна перевага — неблокуючий ввід-вивід: один процес може ефективно обслуговувати багато мережевих з’єднань. Компроміс у тому, що тривалі обчислювально інтенсивні операції можуть блокувати цикл подій (event loop), якщо їх не винести в робочі потоки, окремі процеси або інший сервіс.
JavaScript виконується у V8, а API (Application Programming Interface) Node.js делегують асинхронну роботу операційній системі або пулу потоків libuv. Головний потік JavaScript не чекає завершення операції введення-виведення (I/O); він продовжує виконувати інший код. Коли операція завершується, її зворотний виклик (callback) або продовження Promise стає готовим для циклу подій (event loop). Так Node.js досягає високої конкурентності, хоча користувацький JavaScript зазвичай виконується в одному головному потоці.
V8 компілює й виконує JavaScript, а також керує об'єктами JavaScript і пам'яттю. libuv надає кросплатформний цикл подій, інтеграцію асинхронного I/O та робочий пул потоків для операцій на кшталт частини роботи з файловою системою, DNS і криптографії. Цикл подій визначає, коли мають виконуватися зворотні виклики завершених операцій. Сам Node.js з'єднує JavaScript API з нативними можливостями; сам по собі V8 не надає HTTP-сервер, доступ до файлової системи чи API процесів.
Окремий процес Node.js зазвичай виконує користувацький код JavaScript в одному головному потоці, тому два звичайні обробники запитів не виконують JavaScript в один і той самий час у цьому потоці. Однак платформа не є буквально однопотоковою: `libuv` має пул потоків, операційна система виконує асинхронне введення-виведення (I/O - Input/Output), V8 виконує роботу зі збирання сміття, а Node.js також підтримує `worker_threads` і дочірні процеси. Ця фраза переважно описує стандартну модель виконання JavaScript.
5Як Node.js може одночасно обробляти багато запитів?
Node.js ініціює операції вводу-виводу (I/O - Input/Output), а потім повертається в цикл подій (event loop), замість того, щоб блокувати потік для кожного запиту. Наприклад, 1000 викликів до бази даних можуть виконуватися паралельно, поки головний потік продовжує приймати нові з'єднання та обробляти готові зворотні виклики (callbacks). Це добре працює для навантажень, орієнтованих на ввід-вивід (I/O-bound), таких як HTTP-запити, запити до баз даних, робота з файлами та сокетами. Це працює погано, коли обробники запитів виконують важку синхронну роботу, що навантажує центральний процесор (CPU - Central Processing Unit), оскільки це блокує цикл подій і затримує всі інші запити.
Node.js використовує фази планування libuv, зокрема pending callbacks, idle/prepare, poll, check і close callbacks, а також обробку таймерів. poll обробляє багато зворотних викликів введення-виведення, check виконує виклики setImmediate, а таймери виконують setTimeout/setInterval, коли минули їхні мінімальні затримки та це дозволяє планування. У libuv 1.45 і Node.js 20 та новіших версіях таймери обробляються після poll у кожній ітерації, з початковим проходом таймерів перед входом у цикл для сумісності. process.nextTick використовує окрему чергу Node, а реакції Promise та queueMicrotask — чергу мікрозавдань V8; жодна з них не є фазою libuv. Ці черги виконуються в контрольних точках між зворотними викликами. Рекурсивне поповнення цих черг може позбавити іншу роботу можливості виконуватися.
7Чим відрізняються синхронні, асинхронні та неблокувальні операції?
Синхронність описує потік керування: виклик повертає результат або породжує виняток до того, як викликач продовжить роботу. Асинхронність означає отримання результату пізніше через зворотний виклик, Promise чи інше повідомлення. Блокування означає очікування роботи або ресурсу, під час якого поточний потік не може виконувати іншу роботу; неблокувальні інтерфейси уникають такого очікування, наприклад повертаючи результат «операція заблокувалася б» або організовуючи сповіщення. Це різні характеристики: синхронність сама собою не означає очікування готовності введення-виведення, а повернення Promise не робить важкі синхронні обчислення неблокувальними. У Node.js fs.readFileSync — блокувальний синхронний інтерфейс; fs.promises.readFile надає результат асинхронно, поки головний потік може обробляти іншу роботу. У шляхах обробки запитів віддавай перевагу відповідним асинхронним інтерфейсам введення-виведення.
Голодування циклу подій виникає, коли синхронний JavaScript або черги контрольних точок, що безперервно поповнюються, перешкоджають своєчасній обробці зворотних викликів введення-виведення, таймерів і запитів. Причинами бувають важкі обчислення, великі операції з JSON, рекурсивне планування мікрозавдань і надмірне використання process.nextTick. Обмежуй розмір вхідних даних, профілюй витратні операції та перенось важкі обчислення у worker_threads чи інший сервіс. Розбиваючи роботу на частини, передавай керування наступній ітерації циклу, наприклад через setImmediate; повторюване очікування вже розв'язаних Promise або планування queueMicrotask усе ще може спричиняти голодування введення-виведення. Уникай блокувальних синхронних інтерфейсів у часто виконуваних шляхах обробки запитів.
9Що таке зворотний виклик і що називають пеклом зворотних викликів?
Зворотний виклик — це функція, яку передають іншій операції, щоб та її викликала. Вона може виконуватися синхронно, як у Array.map, або асинхронно, як у fs.readFile. Класичні асинхронні зворотні виклики Node.js зазвичай дотримуються домовленості про помилку в першому аргументі: callback(error, result). Пекло зворотних викликів — це глибоко вкладені залежні виклики, що ускладнюють розуміння потоку керування й обробки помилок. Іменовані функції, менші операції, Promise та 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);
});
`Promise` представляє майбутній результат асинхронної операції та може перебувати у стані очікування (pending), виконання (fulfilled) або відхилення (rejected). Функція `async` завжди повертає `Promise`. Ключове слово `await` призупиняє лише поточну функцію `async`, поки `Promise` не завершиться (settled); воно не блокує весь процес Node.js. Хороша відповідь повинна згадати обробку помилок за допомогою `try/catch`, уникнення "завислих" (floating) `Promise` та використання `Promise.all` для незалежних завдань, щоб операції не виконувалися послідовно без потреби.
11Чим відрізняються process.nextTick, queueMicrotask, setImmediate і setTimeout?
process.nextTick планує роботу в черзі next-tick Node, а queueMicrotask використовує чергу мікрозавдань V8, яка також обслуговує реакції Promise. У звичайній контрольній точці верхнього рівня CommonJS виклики nextTick виконуються раніше за поставлені в чергу виклики Promise/queueMicrotask. На верхньому рівні ES-модуля обчислення вже відбувається в контексті мікрозавдання, тому мікрозавдання з черги можуть виконатися раніше за nextTick. Це залежить від контексту, а не є універсальним правилом пріоритету, що перериває JavaScript під час виконання. setImmediate виконується у check. setTimeout(fn, 0) чекає мінімального часового порога й не виконується негайно. Їхній взаємний порядок залежить від місця планування; якщо обидва заплановані всередині зворотного виклику введення-виведення, setImmediate виконується першим. Для переносимого відкладання роботи в мікрозавдання віддавай перевагу queueMicrotask, якщо не потрібна специфічна поведінка nextTick.
// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
Для синхронних винятків використовуй try/catch. Для операцій на основі Promise виконуй await усередині try/catch або додавай обробник відхилення; зовнішній синхронний try/catch не перехоплює виняток зі зворотного виклику, який виконається пізніше. Зворотні виклики з помилкою в першому аргументі мають перевіряти цей аргумент, а події error в EventEmitter потребують відповідних обробників. Сервери повинні централізувати обробку помилок запитів, журналювати корисний контекст і не розкривати клієнтам внутрішні деталі. Після uncaughtException не можна вважати, що процес здатен безпечно продовжити роботу: виконай лише необхідне синхронне аварійне очищення, заверши процес і дозволь супервізору його перезапустити. Планове коректне завершення корисне за звичайного зупинення, але довільне асинхронне відновлення після неперехопленого винятку не є надійною стратегією.
Модуль — це ізольована одиниця коду з явно експортованим інтерфейсом. Node.js підтримує вбудовані модулі, такі як node:fs та node:http, локальні модулі проєкту, npm-пакети, модулі CommonJS та модулі ECMAScript. Модулі роблять межі відповідальності чіткішими та дозволяють повторне використання коду. Компромісом у робочому середовищі є зв'язність: якщо модулі занадто глибоко імпортують один одного або приховують побічні ефекти під час імпорту, тестування та поведінка під час запуску стають складнішими для аналізу.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Чим відрізняються require у CommonJS та import в ES-модулях?
CommonJS використовує require і module.exports, а ES-модулі — import та export. require зазвичай завантажує синхронно й може викликатися умовно. Статичні декларації import роблять граф залежностей доступним для аналізу; динамічний import() повертає Promise. Сучасні версії Node.js також можуть завантажувати через require граф ES-модулів без await верхнього рівня, відповідно до підтримуваних правил взаємодії. Отже, ES-модулі не означають, що обчислення завжди асинхронне. Режим модуля визначають розширення .cjs/.mjs, поле type у package.json і застосовні правила визначення синтаксису. ES-модулі підтримують await верхнього рівня. CommonJS надає __dirname/__filename; актуальні версії Node.js надають import.meta.dirname/filename для файлових ES-модулів, а import.meta.url корисний для визначення шляхів через URL і підходів, сумісних зі старішими версіями.
// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };
// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
npm — інтерфейс командного рядка для керування пакетами. package.json описує метадані, скрипти й вимоги до прямих залежностей, часто у вигляді діапазонів версій. package-lock.json фіксує розв'язане дерево залежностей із версіями пакетів та відомостями про цілісність. Додавай файл фіксації залежностей застосунку до системи контролю версій. npm ci корисний у безперервній інтеграції: він встановлює залежності за цим файлом, видаляє наявний каталог node_modules і завершується помилкою, якщо package.json не узгоджений із файлом фіксації. Такий файл зменшує небажані зміни залежностей, але не робить однаковими операційні системи, нативні збірки, скрипти життєвого циклу, версії Node чи параметри встановлення. Відтворювані збірки також потребують контрольованих інструментів і середовища.
16Як слід керувати конфігурацією та змінними середовища?
Читай змінні середовища через process.env: наявні значення є рядками, а відсутня властивість дає undefined. Перевіряй обов'язкові значення й цілеспрямовано перетворюй їх під час запуску; саме лише перетворення PORT через Number не відхиляє порожній рядок, NaN або порт за межами допустимого діапазону. Завершуй запуск із зрозумілою помилкою, якщо обов'язкова конфігурація некоректна. Не зберігай справжні секрети в системі контролю версій, а передавай виробничі секрети через платформу розгортання або сховище секретів; приклад конфігурації без секретів можна додавати до репозиторію. NODE_ENV — домовленість застосунку, а не надійна межа безпеки.
const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
Події (Events), буфери (Buffers) та потоки (Streams)
17Що таке `EventEmitter`?
`EventEmitter` реалізує патерн "видавець-підписник" (publish-subscribe) всередині процесу Node.js. Метод `on` реєструє слухача, `once` реєструє слухача для однієї події, `off` видаляє слухача, а `emit` синхронно викликає слухачів у порядку їхньої реєстрації. Синхронна природа є важливою: повільний слухач блокує генератор подій. Подія `error` є особливою, оскільки необроблена подія помилки може призвести до аварійного завершення процесу. Системи з тривалим часом роботи також повинні видаляти невикористовуваних слухачів, щоб уникнути витоків пам'яті.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
Buffer представляє послідовність байтів фіксованого розміру. Він використовується для роботи з файлами, сокетами TCP (Transmission Control Protocol), тілами запитів або відповідей HTTP (Hypertext Transfer Protocol), зображеннями, криптографією та бінарними протоколами. Buffer.from створює буфер з наявних даних, Buffer.alloc створює пам'ять, заповнену нулями, а Buffer.allocUnsafe працює швидше, але може містити стару пам'ять, поки її не буде перезаписано. Це робить allocUnsafe доречним лише тоді, коли код негайно заповнює буфер і ніколи не розкриває застарілі дані.
const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
19Що таке потоки даних (streams) і які існують їх типи?
Потоки даних обробляють дані частинами (chunks), замість того, щоб завантажувати все в пам'ять. Основними типами є `Readable`, `Writable`, `Duplex` та `Transform`. Файли, об'єкти HTTP-запитів та відповідей, TCP-сокети та потоки стиснення gzip є типовими прикладами. Потоки даних є корисними для роботи з великими файлами та мережевим трафіком, оскільки вони зменшують використання пам'яті та затримку. У відповіді рівня продакшену слід зазначити, що помилки в потоках повинні коректно оброблятися по всьому ланцюжку.
20Що таке зворотний тиск (backpressure) і чому корисний метод pipeline?
Зворотний тиск виникає, коли джерело даних створює фрагменти (chunks) швидше, ніж приймач може їх споживати. Без зворотного тиску споживання пам'яті може зростати, доки процес не сповільниться або не завершиться аварійно. Потоки даних та метод `pipe` можуть координувати потік, але метод `pipeline` з модуля `node:stream/promises` є безпечнішим для багатоетапних ланцюжків, оскільки він поширює помилки, закриває пов'язані потоки та повертає `Promise`. Це хороший варіант за замовчуванням для стиснення файлів, завантажень, скачувань та трансформацій потоків.
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')
);