Головна / Статті

Підготовка до інтерв’ю з Node.js та Express: 70 запитань із точними відповідями.

Структурований огляд 70 запитань до інтерв’ю з Node.js та Express, які охоплюють від event loop та стрімів до middleware, безпеки та масштабування, із виправленнями поширених відповідей.

Видавець: REACTAPP.TOP21 хв читання4861 слів

Джерела: Node.js & Express.js Interview Guide

Інтерв’ю на посади з використанням Node.js зазвичай обертаються навколо одних і тих самих тем: як однопотоковий движок обробляє тисячі з’єднань, що насправді робить цикл подій та як Express перетворює примітивний HTTP-сервер на застосунок. Запам’ятовані короткі відповіді руйнуються під час додаткових запитань, тому цей огляд групує 70 поширених запитань за темами та надає для кожної відповіді достатньо обґрунтувань, щоб ви могли її захистити. Якщо популярна відповідь є застарілою чи неточною, це позначається, адже саме виявлення таких деталей часто відрізняє сильного кандидата від підготовленого.

Основи Node.js

Питання 1. Що таке Node.js?

Це середовище виконання, яке запускає JavaScript поза браузером. Воно містить двигун V8 від Google та додає API для роботи з файлами, мережею та процесами. Його модель ґрунтується на подіях із неблокуючим вводом/виводом, що робить його легким та ідеальним для сервісів з великою кількістю даних та у реальному часі.

Питання 2. Чи є Node.js однопотоковим?

Ваш JavaScript виконується на одному основному потоці, який керує циклом подій. Під спудом libuv підтримує пул потоків для завдань, які не мають ефективного асинхронного інтерфейсу операційної системи, таких як виклики файлової системи, dns.lookup, деякі функції crypto та zlib. Тож точна відповідь: однопотоковий для вашого коду, багатопотоковий на рівні системи.

Питання 3. Чому Node.js швидкий?

  • V8 компілює JavaScript у машинний код замість того, щоб інтерпретувати його.
  • Введення та виведення є асинхронними та неблокуючими.
  • Цикл подій забезпечує конкурентність без необхідності створення окремої нитки для кожного запиту.
  • Ця модель чудово підходить для завдань, залежних від мережі та диска; проте для завдань, залежних від CPU, вона є слабкою.
  • Q4. Синхронний та асинхронний код

    Синхронна операція завершується до того, як виконується наступна інструкція, блокуючи все, що йде після неї. Асинхронна операція, така як читання файлу або запит до бази даних, починається та передається на обробку; її виконання продовжується, а результат надсилається пізніше за допомогою функції-відповіді або обіцянки. У наведеній нижче синхронній версії лог-файл чекає на завершення всього процесу читання (і виводить Buffer, оскільки не передано жодного форматування):

    // Synchronous
    const data = fs.readFileSync('file.txt');
    console.log(data);
    

    У асинхронній версії останній рядок логу виводиться до змісту файлу, оскільки процес читання завершується пізніше під час наступного циклу подій:

    // Asynchronous
    fs.readFile('file.txt', (err, data) => {
      console.log(data);
    });
    console.log("This runs first!");
    

    Q5. Що таке npm?

    npm — це менеджер пакетів, який постачається разом із Node.js. Ви використовуєте його для встановлення, публікації та керування залежностями, а він записує їх разом із скриптами у файл package.json.

    Q6. package.json проти package-lock.json

    package.json описує проект: його назву, версію, точку входу, скрипти та діапазони залежностей, які він підтримує. package-lock.json фіксує точну версію кожного встановленого пакета, включаючи транзитивні, щоб кожна машина та система CI встановлювали ідентичну структуру. Зберігайте обидва файли.

    Q7. Які основні модулі вам слід знати?

    Серед найпоширеніше використовуваних вбудованих модулів — fs, path, http та https, os, events, stream, buffer, crypto, util, url, querystring (який тепер вважається застарілим на користь URLSearchParams), child_process, cluster та zlib.

    Q8. require() проти import

    Різниця між CommonJS та ES-модулями постійно піднімається:

    • require завантажується синхронно; ES-модулі завантажуються асинхронно та також дозволяють використовувати динамічний import(), який повертає обіцянку.
    • CommonJS є стандартом для файлів .js; ES-модулі вимагають значення "type": "module" у файлі package.json або розширення .mjs.
  • CommonJS експортує дані за допомогою module.exports; ES modules використовують export та export default.
  • require можна викликати будь-де, навіть всередині умов; статичні оголошення import піднімаються на верхній рівень та мають знаходитися саме там.
  • Q9. module.exports проти exports

    exports спочатку є посиланням на той самий об’єкт, що й module.exports. Додавання до нього властивостей працює, але присвоєння нового значення (exports = {...}) лише змінює локальну змінну, а модуль все одно експортує початковий об’єкт. Щоб замінити весь експорт, потрібно присвоїти значення module.exports.

    Q10. Що таке REPL?

    REPL — це скорочення від Read, Eval, Print, Loop. Запуск node без аргументів відкриває інтерактивну оболонку, яка обчислює кожне вираз, який ви введете, та виводить результат, що зручно для швидких експериментів.

    Цикл подій та асинхронні патерни

    П11. Що таке цикл подій?

    Це механізм, який дозволяє одному потоку JavaScript виконувати неблокуючі операції введення/виведення. Коли стек викликів порожній, цикл бере наступний готовий калебек зі своїх черг та виконує його. Більш детальний опис того, як він взаємодіє з libuv, наведено у нашій статті про libuv, цикл подій та пул потоків.

    П12. Які є фази циклу подій?

    1. Таймери: калебеки від setTimeout та setInterval, час виконання яких настав.
  • У підготовці калебеки: деякі калебеки системних операцій відкладені з попередньої ітерації.
  • Спокійний режим та підготовка: внутрішнє обслуговування.
  • Збір даних: збирає ново завершені події введення/виведення та запускає їхні калебеки.
  • Перевірка: калебеки від setImmediate().
  • Калебеки закриття: обробники, такі як socket.on('close').
  • Очереді мікрозавдань (process.nextTick спочатку, потім реакції на обіцянки) спорожнюються після кожного окремого калебека, а не лише між фазами. Така поведінка існує з Node 11, і це поширене запитання.

    Q13. nextTick, promises, setTimeout та setImmediate

    • Калебеки від process.nextTick() виконуються як тільки завершується поточна операція, раніше за мікрозавдання обіцянок.
  • Обіцяні реакції, такі як .then, виконуються, коли черга nextTick спорожніла, але ще до того, як цикл перейде до наступного кроку.
  • setTimeout(fn, 0) виконується під час фази таймерів, як тільки мине мінімальна затримка.
  • setImmediate() виконується під час фази перевірки, відразу після фази опитування тієї самої ітерації.
  • Цей скрипт використовує усі чотири методи:

    console.log('start');
    setTimeout(() => console.log('setTimeout'), 0);
    setImmediate(() => console.log('setImmediate'));
    process.nextTick(() => console.log('nextTick'));
    Promise.resolve().then(() => console.log('promise'));
    console.log('end');
    

    Очікуваний результат показує спочатку синхронні записи, а потім мікрозавдання. Два макрозавдання можуть з’явитися в будь-якому порядку під час запланування з головного модуля, оскільки це залежить від того, чи минула порогова значення таймера на момент початку циклу; всередині функції-колбека файлу чи сокета setImmediate завжди виконується першим:

    // Output:
    // start
    // end
    // nextTick
    // promise
    // setTimeout (or setImmediate — order can vary if not inside I/O)
    // setImmediate (or setTimeout)
    

    Q14. Колбеки та пекло колбеків

    Колбек — це функція, яку ви передаєте операції, щоб її можна було викликати після завершення цієї операції. Коли кожен крок залежить від попереднього, колбеки накладаються один на одного все глибше та глибше, створюючи піраміду, відому як «пекло колбеків», що ускладнює читання та обробку помилок:

    getUser(id, (user) => {
      getPosts(user, (posts) => {
        getComments(posts, (comments) => {
          // deeply nested - hard to manage
        });
      });
    });
    

    Об’єкти Promise та синтаксис async/await спрощують цю структуру.

    Q15. Що таке Promise?

    Promise — це об’єкт, який представляє майбутній результат асинхронної операції. Він може бути у стані очікування, виконаним із певним значенням або відхиленою з причиною, і після того, як його статус визначений, він більше не змінюється. Обробники подій приєднуються за допомогою методів .then та .catch:

    const promise = new Promise((resolve, reject) => {
      if (success) resolve(data);
      else reject(error);
    });
    promise.then(data => console.log(data)).catch(err => console.error(err));
    

    Q16. Що таке async/await?

    Це синтаксис, заснований на обіцянках, який дозволяє асинхронному коду виконуватися зверху вниз. Функція async завжди повертає обіцянку, а await зупиняє цю функцію до тих пір, поки очікувана обіцянка не буде виконана, причому відмови проявляються у вигляді винятків, які можна спіймати:

    async function getUser() {
      try {
        const res = await fetch('/api/user');
        const data = await res.json();
        return data;
      } catch (err) {
        console.error(err);
      }
    }
    

    Зауважте, що в цьому прикладі помилка записується у журнал, і функція неявно повертає undefined, тому викликаючі не можуть відрізнити невдачу від порожнього результату. Часто кращим вибором є перекидання винятку.

    Q17. Комбінатори обіцянок

    • Promise.all() виконується, коли всі елементи виконані, і відмовляється як тільки хоча б один елемент відмовиться.
    • Promise.race() завершується тим елементом, який завершиться першим, незалежно від того, чи це виконання чи відмова.
    • Promise.allSettled() чекає на всі елементи та повідомляє про кожен результат.
  • Promise.any() виконується з першим задоволеним елементом та відхиляється (з помилкою AggregateError) лише у разі, якщо всі елементи відхилені.
  • Q18. Блокуючий та неблокуючий код

    Блокуючий код зупиняє всі інші операції до завершення своєї роботи, як це робить fs.readFileSync. Неблокуючий код починає виконання та дозволяє програмі продовжувати роботу, як це робить fs.readFile. На сервері один блокуючий виклик затримує всі поточні запити.

    Стріми, буфери та події

    Q19. Що таке стріми?

    Стріми обробляють дані поступово, порціями, замість того щоб зберігати їх усі в пам’яті. Вони є ідеальним інструментом для великих файлів, медіаконтенту та мережевого трафіку. Існує чотири типи:

    • Читабельні, наприклад fs.createReadStream
    • Записувані, наприклад fs.createWriteStream
    • Дуплексний режим, який дозволяє одночасно читати та записувати дані, як-от TCP-сокет
    • Трансформація – дуплексний потік, який змінює дані під час їх передачі, як-от zlib.createGzip

    Q20. Що таке буфер?

    Буфер зберігає необроблені бінарні дані в пам’яті, виділеній окремо від купи V8. Ви стикаєтесь з ним там, де дані представлені у вигляді байтів, а не рядків чи об’єктів: потоки, файли, сокети. Створення буфера з рядка відображає базові значення байтів у шістнадцятковому форматі:

    const buf = Buffer.from('Hello');
    console.log(buf); // <Buffer 48 65 6c 6c 6f>
    

    Q21. Що таке EventEmitter?

    EventEmitter з модуля events реалізує паттерн спостерігача (pub/sub), на якому будується більша частина API Node. Ви створюєте його екземпляр:

    const EventEmitter = require('events');
    const emitter = new EventEmitter();
    

    потім реєструєте слухачів за допомогою on та активуєте їх за допомогою emit, передаючи всі аргументи:

    emitter.on('greet', (name) => console.log(`Hello, ${name}`));
    emitter.emit('greet', 'John');
    

    Слухачі виконуються синхронно, у порядку реєстрації, коли викликається emit.

    Q22. Як обробляти великі дані, не вичерпуючи пам’ять?

    Використовуйте потоки: читайте невеликі фрагменти та передавайте потік для читання у потік для запису, щоб використання пам’яті залишалося приблизно постійним незалежно від розміру файлу:

    fs.createReadStream('input.txt').pipe(fs.createWriteStream('output.txt'));
    

    Для коду в продакшені stream.pipeline є кращим варіантом, ніж .pipe(), оскільки він передає помилки та очищує кожен потік у ланцюзі.

    Більш глибокі концепції часу виконання

    Q23. process.nextTick() проти setImmediate(), детально

    nextTick-колбеки виконуються перед тим, як цикл зможе продовжитися, навіть перед мікрозавданнями типу promise. setImmediate-колбеки чекають на фазу перевірки, після фази опитування. Оскільки кожен nextTick може запланувати ще один, рекурсивна ланцюгова структура може «загнобити» цикл, через що заплановані операції читання та запису ніколи не будуть виконані. Цей спосіб збою розглядається у статті про те, як process.nextTick тихо «загноблює» цикл подій Node.js.

    Q24. Що таке кластерування та навіщо використовувати його?

    Один процес Node використовує один ядро для обробки JavaScript. Модуль cluster створює кілька робочих процесів, кожен з яких має власний цикл подій та пам’ять, і дозволяє їм ділитися одним портом сервера, щоб усі ядра машини обробляли трафік. Модулі імпортуються так:

    const cluster = require('cluster');
    const os = require('os');
    

    Потім з основного процесу створюється по одному працівнику на кожен CPU, поки працівники завантажують сервер:

    if (cluster.isMaster) {
      os.cpus().forEach(() => cluster.fork());
    } else {
      require('./server');
    }
    

    cluster.isMaster все ще працює, але вже застарів на користь cluster.isPrimary; у новому коді слід використовувати останній.

    Q25. cluster versus worker_threads

    • cluster створює окремі процеси з ізольованою пам’яттю, що ідеально підходить для масштабування HTTP-серверів.
    • worker_threads створює потоки всередині одного процесу. Вони можуть ділитися пам’яттю через SharedArrayBuffer або передавати буфери, що підходить для завдань із високим навантаженням на CPU, таких як обробка зображень.

    Q26. Як обробляються завдання із високим навантаженням на CPU?

    Код, який інтенсивно використовує CPU, блокує цикл подій, тому його потрібно перемістити в інше місце: у worker_threads, у дочірній процес або повністю поза сервісом за допомогою черги та фонових робітників. Дивіться способи перенесення обчислень на CPU за допомогою worker threads та пулів для прикладів реалізації.

    Q27. Що таке мідлвейр загалом?

    Мідлвейр — це код, який знаходиться між отриманням запиту та надсиланням відповіді, де він може перевіряти, трансформувати, верифікувати або фіксувати інформацію. Express будує всю свою архітектуру навколо цієї ідеї, як показано у наступних розділах.

    Q28. Що таке дочірні процеси?

    Модуль child_process запускає зовнішні програми або інші скрипти Node у окремих процесах. Важливими є чотири функції:

    • spawn() передає результати у потоці, тому добре підходить для обробки великих об’ємів даних.
  • exec() виконує команди через оболонку та зберігає результат у буфері, що підходить для невеликих об’ємів даних.
  • execFile() виконує файл безпосередньо, без використання оболонки, що робить його безпечнішим при роботі з ненадійними аргументами.
  • fork() — це спеціалізована версія spawn для модулів Node, яка створює канал IPC для обміну повідомленнями.
  • Q29. Що таке libuv?

    libuv — це бібліотека на мові C, яка забезпечує цикл подій Node, асинхронний ввод/вивод та пул потоків, приховуючи відмінності між механізмами сповіщень про події в Linux, macOS та Windows.

    Q30. setTimeout versus setInterval

    setTimeout(fn, delay) виконує fn один раз через принаймні delay мілісекунд. setInterval(fn, delay) виконує його повторно кожні ці інтервали, доки ви не викличете clearInterval().

    Q31. Обробка непійманих винятків та нерозглянутих відмов

    Існують обробники на рівні процесу для обох ситуацій. Для синхронних винятків, яких ніхто не спіймав, потрібно записати лог та завершити роботу, оскільки процес може перебувати у нестабільному стані:

    process.on('uncaughtException', (err) => {
      console.error('Uncaught Exception:', err);
      process.exit(1); // recommended: exit after cleanup
    });
    

    Для обіцянок, які були відхилені без обробника:

    process.on('unhandledRejection', (reason, promise) => {
      console.error('Unhandled Rejection:', reason);
    });
    

    З версії Node 15 нерозглянута відмова за замовчуванням призводить до завершення процесу, тому реєстрація цього слухача змінює таку поведінку. Розглядайте обидві ситуації як останній засіб для запису логів, а не як частину контролю потоку; використовуйте try/catch та .catch() безпосередньо в коді.

    Q32. Що таке витік пам’яті та як його знайти?

    Витік пам’яті — це пам’ять, яка залишається доступною після того, як вона більше не потрібна: зростаючі глобальні кеші, таймери чи слухачі, які ніколи не видаляються, а також замикання, що зберігають великі об’єкти. Діагностуйте це за допомогою флага --inspect та знімків купи пам’яті в Chrome DevTools, або інструментів на кшталт heapdump та clinic.js, порівнюючи знімки з часом.

    Q33. __dirname versus __filename

    __dirname — це абсолютний шлях до папки, що містить поточний модуль; __filename — це абсолютний шлях до самого файлу. Обидва існують лише в CommonJS. У ES-модулях їх можна отримати з import.meta.url, або використовувати import.meta.dirname та import.meta.filename у новіших версіях Node.

    Q34. Чому використовувати модуль шляхів?

    path дозволяє створювати та аналізувати шляхи до файлів у портабельний спосіб за допомогою функцій таких як path.join, path.resolve, path.basename та path.extname; таким чином вам не доводиться вручну об’єднувати рядки, що уникає проблем із знаками слеша в Windows та Unix.

    Q35. fs.readFile versus fs.createReadStream

    fs.readFile завантажує весь файл у пам’ять перед тим, як повернутися, що є ризикованим для великих файлів. fs.createReadStream передає файл порціями, зберігаючи невеликий обсяг пам’яті та дозволяючи почати обробку негайно.

    Основи Express

    Q36. Що таке Express?

    Express — це мінімалістична, нейтральна за своїми принципами веб-фреймворк, створений на основі модуля http Node. Він додає функції маршрутизації, систему проміжних компонентів та інтеграцію з движком шаблонів, усуваючи більшу частину зайвого коду для обробки запитів.

    Q37. Базовий сервер Express

    Створіть об’єкт додатку:

    const express = require('express');
    const app = express();
    

    потім зареєструйте маршрут та почніть прослуховування:

    app.get('/', (req, res) => {
      res.send('Hello World');
    });app.listen(3000, () => console.log('Server running on port 3000'));
    

    Q38. Що таке мідлвейр у Express?

    Мідлвейр Express — це функція, яка отримує параметри req, res та next. Вона може виконувати будь-який код, модифікувати запит чи відповідь, завершувати обробку шляхом надсилання відповіді чи викликати next(), щоб передати керування наступній функції. Класичним прикладом є логер запитів:

    app.use((req, res, next) => {
      console.log(`${req.method} ${req.url}`);
      next();
    });
    

    Якщо мідлвейр не надсилає відповідь та не викликає next(), запит застрягає.

    Q39. Види мідлвейрів Express

    1. На рівні додатку, підключаються за допомогою app.use() або app.METHOD().
    2. На рівні маршрутизатора, підключаються до екземпляра express.Router().
  • Обробка помилок, яку можна розпізнати за чотирма параметрами (err, req, res, next).
  • Вбудовані функції, такі як express.json() та express.static().
  • Функції від сторонніх розробників, такі як cors, morgan та helmet.
  • Q40. Як працює маршрутизація?

    Маршрут поєднує метод HTTP із шляхом та обробником. Типовий ресурс CRUD виглядає так:

    app.get('/users', (req, res) => { /* get all users */ });
    app.post('/users', (req, res) => { /* create user */ });
    app.put('/users/:id', (req, res) => { /* update user */ });
    app.delete('/users/:id', (req, res) => { /* delete user */ });
    

    Q41. Що таке express.Router()?

    Маршрутизатор — це самодостатня міні-прикладка з власними маршрутами та проміжним програмуванням, яку можна розмістити на певному шляху. Він допомагає організувати великі прикладки за функціями. Маршрути визначаються у окремому модулі:

    // userRoutes.js
    const router = require('express').Router();
    router.get('/', (req, res) => res.send('All users'));
    module.exports = router;
    

    а потім його розміщують у основній прикладці, де його / стає /users:

    // app.js
    app.use('/users', require('./userRoutes'));
    

    Q42. app.use() проти app.get() та інших

    app.use() відповідає кожному методу HTTP та будь-якому шляху, який починається з заданого префікса, саме тому він встановлює мідлвейри та роутери. app.get(), app.post() та подібні функції відповідають одному методу та повному шаблону маршруту.

    Q43. req.params, req.query та req.body

    • req.params містить іменовані сегменти маршруту: /users/:id повертає req.params.id.
    • req.query містить значення рядка запиту: /users?age=25 повертає req.query.age, який завжди є рядком.
    • req.body містить оброблений вміст запиту для методів на кшталт POST та PUT; він залишається невизначеним, якщо спочатку не виконується мідлвейр для обробки тіла запиту, наприклад express.json().

    Q44. Обробка JSON та даних форми

    Два вбудовані парсери покривають більшість випадків: один для тіл у форматі JSON, а інший — для HTML-форм у кодованому вигляді:

    app.use(express.json());               // parses application/json
    app.use(express.urlencoded({ extended: true })); // parses form data
    

    Q45. Що таке express.static()?

    Це вбудований мідлвейр, який обслуговує файли такі як зображення, таблиці стилів, скрипти та HTML з певного каталогу:

    app.use(express.static('public'));
    

    Проміжний Express

    Q46. Обробка помилок

    Помилки обробляються за допомогою мідлвейру з чотирма параметрами, який реєструється після всіх маршрутів:

    app.use((err, req, res, next) => {
      console.error(err.stack);
      res.status(500).json({ message: 'Something went wrong' });
    });
    

    Усередині маршрутів та мідлвейрів передавайте інформацію про помилки за допомогою next(err):

    app.get('/user/:id', async (req, res, next) => {
      try {
        const user = await getUser(req.params.id);
        res.json(user);
      } catch (err) {
        next(err); // passed to error-handling middleware
      }
    });
    

    У продакшені уникайте надсилання err.stack чи внутрішніх повідомлень клієнтам.

    Q47. Обробка статусу 404

    У Express немає вбудованої підтримки JSON для статусу 404. Додайте універсальний мідлвейр після кожного маршруту; якщо запит досягає його, жоден інший маршрут не підходить:

    app.use((req, res) => {
      res.status(404).json({ message: 'Route not found' });
    });
    

    Q48. CORS та пакет cors

    Cross-Origin Resource Sharing – це механізм браузера, який вирішує, чи може сторінка з одного джерела отримувати дані у відповідь від іншого. Браузер забезпечує його дотримання; сервер увімкнює цю функцію через заголовки відповіді, які пакет cors налаштовує за вас:

    const cors = require('cors');
    app.use(cors()); // allow all origins
    app.use(cors({ origin: 'https://example.com' })); // restrict origin
    

    У цьому уривку показано два варіанти: дозволити кожному джерелу чи лише одному; використовуйте один з них, а не обидва, оскільки правило більшого пропуску буде застосовуватися першим.

    Q49. Сесії та куки

    Завантажте середовище для роботи з куками та сесіями:

    const cookieParser = require('cookie-parser');
    const session = require('express-session');
    

    потім зареєструйте їх із секретним ключем для підпису та відповідними параметрами:

    app.use(cookieParser());
    app.use(session({
      secret: 'mySecretKey',
      resave: false,
      saveUninitialized: true
    }));
    

    Зберігайте секрет у змінній середовища, а не в коді, та врахуйте параметр saveUninitialized: false, щоб уникнути створення сесій для анонімних відвідувачів. Стандартний внутрішній сховище не призначене для використання у продакшні.

    Q50. Аутентифікація JWT

    Імпортуємо бібліотеку:

    const jwt = require('jsonwebtoken');
    

    У наведеному нижче коді під час входу створюється токен із ідентифікатором користувача, шляхи захищаються за допомогою мідлверу, який витягує токен-носій з заголовка Authorization та перевіряє його, а розшифрований вміст приєднується до req.user:

    // Generate token on login
    const token = jwt.sign({ id: user.id }, 'secretKey', { expiresIn: '1h' });// Middleware to protect routes
    function auth(req, res, next) {
      const token = req.headers.authorization?.split(' ')[1];
      if (!token) return res.status(401).json({ message: 'No token' });
      try {
        req.user = jwt.verify(token, 'secretKey');
        next();
      } catch (err) {
        res.status(403).json({ message: 'Invalid token' });
      }
    }app.get('/protected', auth, (req, res) => res.json({ data: 'secret data' }));
    

    Як і у випадку з сесіями, значення 'secretKey', вказане явно, має походити з конфігурації. Загальноприйнятою практикою є повернення коду 401 у разі відсутності токена та коду 403 у разі його недійсності, хоча багато API повертають код 401 у обох випадках.

    Q51. Перевірка запиту

    Бібліотеки на кшталт express-validator чи joi визначають правила та збирають помилки. За допомогою express-validator імпортуються відповідні інструменти:

    const { body, validationResult } = require('express-validator');
    

    потім перед обробником запиту послідовно використовуються мідлвери з правилами та перевіряється результат:

    app.post('/user',
      body('email').isEmail(),
      body('password').isLength({ min: 6 }),
      (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
        // proceed
      }
    );
    

    Щоб детальніше порівняти підходи до валідації, дивіться Zod проти express-validator.

    Q52. Завантаження файлів

    multer обробляє формат multipart/form-data. Налаштуйте місце зберігання:

    const multer = require('multer');
    const upload = multer({ dest: 'uploads/' });
    

    і застосуйте його для кожного маршруту; upload.single('file') очікує один файл у полі під назвою file та робить його доступним як req.file:

    app.post('/upload', upload.single('file'), (req, res) => {
      res.json({ file: req.file });
    });
    

    У реальних системах також необхідно обмежувати розмір та тип файлів, а також ніколи не довіряти імену файлу, наданому клієнтом.

    Q53. Підключення до MongoDB за допомогою Mongoose

    Створіть з’єднання:

    const mongoose = require('mongoose');
    mongoose.connect('mongodb://localhost:27017/mydb');
    

    потім визначте модель та використовуйте її у маршруті:

    const User = mongoose.model('User', new mongoose.Schema({
      name: String,
      email: String
    }));app.post('/users', async (req, res) => {
      const user = await User.create(req.body);
      res.status(201).json(user);
    });
    

    Передача req.body безпосередньо до функції create дозволяє клієнтам встановлювати будь-які поля схеми, тому спочатку необхідно перевірити дані або вибрати дозволені поля.

    Q54. Порядок виконання мідлвейрів

    Express виконує мідлвейри у порядку їх реєстрації, зверху вниз, і ланцюг зупиняється при першій функції, яка не викликає next(). Саме через цей порядок обробники помилок 404 та інші обробники помилок розташовуються в кінці.

    Q55. Механізми шаблонів

    Механізми шаблонів генерують HTML на основі даних з серверної сторони. Популярними варіантами є EJS, Pug (раніше Jade) та Handlebars. Щоб їх налаштувати, потрібно вказати параметри двигуна шаблонів та викликати res.render:

    app.set('view engine', 'ejs');
    app.get('/', (req, res) => res.render('index', { name: 'John' }));
    

    Розширені можливості Express та Node

    Q56. Асинхронні помилки в Express 4 порівняно з Express 5

    У Express 4 відхилення обіцянки від async-обробника не ловиться фреймворком; без try/catch разом із next(err) або обгортки це стає некерованим відхиленням. Express 5 автоматично передає відхилені обіцянки від обробників та проміжкового програмного забезпечення до обробника помилок. Звичайним рішенням у Express 4 є використання невеликої функції вищого порядку:

    // Express 4 workaround
    const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
    

    Обгортка обробника цією функцією направляє будь-яке відхилення до next:

    app.get('/data', asyncHandler(async (req, res) => {
      const data = await fetchData(); // if this throws, asyncHandler catches it
      res.json(data);
    }));
    

    Цей паттерн детально описаний у видаленні шаблонного коду try/catch за допомогою asyncHandler.

    Q57. Структурування великого додатку Express

    Типова структура у стилі MVC розділяє функції на папки:

    project/
    ├── controllers/     # business logic
    ├── routes/          # route definitions
    ├── models/          # DB schemas
    ├── middlewares/      # custom middleware
    ├── config/          # DB, env configs
    ├── utils/           # helper functions
    ├── app.js
    └── server.js
    

    Маршрути оголошують кінцеві точки та передають обробку контролерам, контролери містять логіку обробки запитів, а моделі інкапсулюють доступ до даних. Розділення app.js (налаштування додатку) від server.js (запуск додатку) також полегшує його імпорт у тестах.

    Q58. Обмеження частоти запитів

    express-rate-limit підраховує кількість запитів від кожного клієнта протягом певного періоду. Імпортуйте його:

    const rateLimit = require('express-rate-limit');
    

    і застосуйте обмежувач – тут 100 запитів від кожного IP щогодини:

    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100 // limit each IP to 100 requests per window
    });app.use(limiter);
    

    Стандартний сховище знаходиться в пам’яті, тому кожен процес у кластері підраховує дані окремо; при масштабуванні використовуйте спільне сховище, таке як Redis. За проксі налаштуйте параметр trust proxy, щоб використовувалася справжня IP-адреса клієнта.

    Q59. Забезпечення безпеки додатку Express

    • Встановіть захисні HTTP-заголовки за допомогою helmet.
  • Перевіряйте та очищуйте всі вхідні дані, щоб запобігти виконанню SQL чи NoSQL ін’єкцій та XSS.
  • Надавайте контент через HTTPS.
  • Впроваджуйте обмеження швидкості запитів.
  • Завантажуйте конфіденційні дані з середовища, наприклад за допомогою dotenv, замість того, щоб зберігати їх у коді.
  • Стежте за оновленнями залежностей та виконуйте npm audit.
  • Додайте захист від CSRF для додатків, які використовують куки та форми.
  • Q60. res.send() проти res.json() проти res.end()

    • res.send() приймає рядки, об’єкти чи буфери та вибирає відповідний тип Content-Type.
    • res.json() серіалізує свій аргумент у форматі JSON та встановлює Content-Type: application/json.
    • res.end() завершує відповідь без додаткових даних, що є простим обгортком навколо вбудованого методу Node.

    Q61. Що робить next('route')?

    Він пропускає решту функцій-викликів поточного маршруту та переходить до наступного відповідного маршруту. Це стосується лише обробників, зареєстрованих через app.METHOD() або router.METHOD(), які мають кілька функцій-викликів.

    Q62. Покращення продуктивності

    • Запускайте по одному процесу на кожен ядро за допомогою клустеризації або PM2.
    • Зберігайте відповіді у кеші Redis або в пам’яті.
    • Стискайте відповіді за допомогою проміжку compression, або нехай це робить проксі.
    • Розмістіть перед собою Nginx або інший зворотний проксі чи балансувальник навантаження.
    • Уникайте виконання ресурсоємких операцій на CPU у рамках циклу подій.
    • Використовуйте пул з’єднань до бази даних.
    • Розгляньте використання HTTP/2.
    • Використовуйте асинхронний ввод/вивід у всьому та уникайте версій Sync у шляхах запитів.

    Q63. REST проти GraphQL з Express

    REST надає багато кінцевих точок із фіксованою структурою відповідей, тому клієнти часто отримують занадто багато або занадто мало даних. GraphQL надає одну кінцеву точку, де клієнт може точно описати потрібні йому дані. Його зазвичай інтегрують у Express за допомогою express-graphql або apollo-server-express; проте обидві ці бібліотеки вже застаріли на користь новіших інтеграцій, тому перевірте актуальну документацію Apollo чи GraphQL Yoga.

    Q64. Як Express обробляє одночасні запити?

    Кожен запит — це просто низка функцій-викликів у циклі подій. Оскільки запити до баз даних та мережі є неблокуючими, одна потік може обробляти багато запитів одночасно: мережевий трафік передається на асинхронні механізми операційної системи, робота з файлами — на пул потоків libuv, а функції-виклики виконуються при надходженні результатів. На відміну від класичних багатопотокових серверів, де є окремий потік на кожен запит, тут цього немає, і саме тому один повільний синхронний обробник впливає на всі запити.

    Q65. PM2 проти модуля cluster

    cluster — це вбудований інструмент для масштабування за допомогою кількох процесів. PM2 — це менеджер процесів, створений на основі тієї ж ідеї, який додає автоматичне перезапуск після збоїв, балансування навантаження, обробку журналів, безперервну заміну коду та моніторинг, тому він широко використовується у багатьох продакшн-системах.

    Q66. Чому потрібен зворотний проксі перед Node?

    Обернений проксі, такий як Nginx, завершує процес TLS-шифрування, розподіляє навантаження між інстанціями, використовує кеш, стискає та подає статичні файли, тим самим звільняючи Node від цих обов’язків, щоб процес додатку міг зосередитися на бізнес-логіці.

    Q67. Конфігурація, специфічна для середовища

    Пакет dotenv завантажує змінні з файлу .env у process.env, і додаток переходить на значення за замовчуванням, якщо певне значення відсутнє:

    require('dotenv').config();
    const PORT = process.env.PORT || 3000;
    

    Новіші версії Node також можуть завантажувати файли середовища нативно за допомогою флага --env-file. Ніколи не комітуйте файли .env, які містять конфіденційну інформацію.

    Q68. app.all() проти app.use()

    app.all() відповідає кожному методу HTTP на певному шляху маршруту. app.use() також відповідає кожному методу, але для всього шляху та всього, що знаходиться під ним, саме тому він використовується для підключення мідлверів.

    Q69. Сторінкування з Express та MongoDB

    Читайте значення page та limit з рядка запиту з розумними значеннями за замовчуванням, а потім поєднуйте skip та limit:

    app.get('/posts', async (req, res) => {
      const page = parseInt(req.query.page) || 1;
      const limit = parseInt(req.query.limit) || 10;
      const posts = await Post.find()
        .skip((page - 1) * limit)
        .limit(limit);
      res.json(posts);
    });
    

    Підготуйте код до використання в продакшені: передавайте параметр основи для parseInt, обмежуйте значення page мінімум 1, а limit — максимальним значенням, а також додайте стабільний параметр sort. Для дуже великих колекцій швидкість обробки skip знижується зі збільшенням офсету, тому сторінкування за допомогою курсора на індексованому полі є ефективнішим.

    Q70. Що таке ідемпотентність?

    Операція є ідемпотентною, коли її повторення залишає сервер у тому самому стані, що й одноразове виконання. GET, PUT та DELETE визначені як ідемпотентні; POST зазвичай не є таким, оскільки кожен виклик може створити новий ресурс. Повторні спроби після закінчення тайм-ауту є безпечними лише для ідемпотентних запитів, тому API оплати часто додають ключі ідемпотентності до запитів типу POST.

    Основні висновки

    • Більшість запитань щодо Node зводяться до однієї моделі: єдиний потік JavaScript, цикл подій із визначеними фазами, мікрозавдання, які обробляються після кожного калебеку, та libuv, який відповідає за решту.
    • Більшість запитань щодо Express зводяться до іншої моделі: послідовна ланцюг мідлверів, де кожна функція або відповідає, або викликає next(), а помилки передаються до обробників із чотирма аргументами.
  • Інтерв’юери цінують точність щодо деталей, які змінилися з часом, таких як cluster.isPrimary, ситуації збою при некерованих відмовах, асинхронні помилки Express 5 та допоміжні функції для обробки шляхів у ESM.
  • До кожної відповіді слід додати зауваження щодо реальних умов використання (зберігання секретів у конфігурації, передача помилок, спільні механізми обмеження кількості запитів), адже саме це зазвичай є наступним запитанням.