Главная / Статьи

Подготовка к собеседованию по Node.js и Express: 70 вопросов с точными ответами

Структурированный обзор 70 вопросов к собеседованию по Node.js и Express, охватывающий темы от цикла событий и потоков до промежуточных модулей, безопасности и масштабирования, с исправлениями распространенных ответов.

Издатель: 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.
  • Вопрос 4. Синхронный и асинхронный код

    Синхронная операция завершается до выполнения следующей строки, блокируя всё, что находится после неё. Асинхронная операция, такая как чтение файла или запрос к базе данных, запускается и передаётся на обработку; выполнение продолжается, а результират возвращается позже с помощью функции-возврата или обещания. В приведённой ниже синхронной версии лог-файл ждёт завершения всего процесса чтения (и выводит объект 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!");
    

    Вопрос 5. Что такое npm?

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

    Вопрос 6. package.json против package-lock.json

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

    Вопрос 7. Какие основные модули следует знать?

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

    Вопрос 8. require() против import

    Различия между CommonJS и ES-модулями постоянно поднимаются:

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

    exports изначально является ссылкой на тот же объект, что и module.exports. Добавление к нему свойств работает, но присваивание нового значения (exports = {...}) лишь переопределяет локальную переменную, при этом модуль по-прежнему экспортирует исходный объект. Чтобы заменить весь экспорт, необходимо присвоить значение module.exports.

    Вопрос 10. Что такое REPL?

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

    Цикл событий и асинхронные паттерны

    В11. Что такое цикл событий?

    Это механизм, позволяющий единственной нити JavaScript выполнять неблокирующие операции ввода/вывода. Когда стек вызовов пуст, цикл берёт следующий готовый к выполнению обратный вызов из очередей и запускает его. Более подробное описание взаимодействия цикла событий с libuv приведено в нашей статье о libuv, цикле событий и пуле потоков.

    В12. Какие существуют фазы цикла событий?

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

    В13. nextTick, обещания, 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 упрощают эту структуру.

    В15. Что такое 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));
    

    В16. Что такое 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) только в случае, если все значения отклонены.
  • Вопрос 18. Блокирующий и неблокирующий код

    Блокирующий код останавливает всё остальное до завершения своей работы, как это делает fs.readFileSync. Неблокирующий код начинает выполнение задачи и позволяет программе продолжать работу, как это делает fs.readFile. На сервере один блокирующий вызов замедляет обработку всех текущих запросов.

    Потоки, буферы и события

    Вопрос 19. Что такое потоки?

    Потоки обрабатывают данные постепенно, пакет за пакетом, вместо того чтобы хранить их все в памяти. Они являются подходящим инструментом для больших файлов, медиаконтента и сетевого трафика. Существует четыре вида потоков:

    • Читаемые, например fs.createReadStream
    • Записываемые, например fs.createWriteStream
    • Дуплексный режим, позволяющий одновременно читать и записывать данные, как в TCP-сокете
    • Преобразование — дуплексный поток, который изменяет данные в процессе их передачи, например zlib.createGzip

    Вопрос 20. Что такое буфер?

    Буфер хранит необработанные двоичные данные в памяти, выделенной вне кучи V8. С ним сталкиваемся там, где данные представлены в виде байтов, а не строк или объектов: потоки, файлы, сокеты. Создание буфера из строки позволяет увидеть исходные значения байтов в шестнадцатеричном формате:

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

    Вопрос 21. Что такое 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.

    Вопрос 22. Как обрабатывать большие объемы данных, не исчерпывая память?

    Используйте потоки: читайте небольшие фрагменты и перенаправляйте поток для чтения в поток для записи, чтобы использование памяти оставалось примерно постоянным независимо от размера файла:

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

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

    Более глубокие концепции выполнения

    Вопрос 23. process.nextTick() против setImmediate(), подробно

    nextTick-колбэки выполняются до того, как цикл сможет продолжиться, даже раньше микрозадач на основе promise. setImmediate-колбэки ждут фазы проверки, которая следует за фазой опроса. Поскольку каждый nextTick может запланировать ещё один, рекурсивная цепочка может заблокировать цикл, в результате чего ожидающиеся операции чтения и записи так и не будут обработаны. Этот сценарий сбоя рассматривается в статье как process.nextTick тихо блокирует цикл событий Node.js.

    Вопрос 24. Что такое кластеризация и зачем её использовать?

    Один процесс 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; используйте последний в новом коде.

    Вопрос 25. cluster против worker_threads

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

    Вопрос 26. Как обрабатываются задачи с высокой нагрузкой на CPU?

    Код, требующий больших вычислительных ресурсов CPU, блокирует цикл событий, поэтому его следует переместить в другое место: в worker_threads, в дочерний процесс или полностью вынести из сервиса с помощью очереди и фоновых рабочих процессов. Примеры решений смотрите в статье о переносе нагрузки на CPU с использованием worker threads и пулов.

    Вопрос 27. Что такое мидлвэр в общем?

    Мидлвэр — это код, который находится между приемом запроса и отправкой ответа, где он может осматривать, преобразовывать, проверять или вести логирование. Вся архитектура Express строится на этой идее, как показано в следующих разделах.

    Вопрос 28. Что такое дочерние процессы?

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

    • spawn() обрабатывает потоки вывода, поэтому хорошо справляется с большими объемами данных.
  • exec() выполняет команды через оболочку и сохраняет результат в буфер, что подходит для небольших объемов данных.
  • execFile() запускает файл напрямую без использования оболочки, что делает процесс более безопасным при работе с ненадежными аргументами.
  • fork() — это специализированная версия spawn для модулей Node, предназначенная для создания канала взаимодействия между процессами для передачи сообщений.
  • Вопрос 29. Что такое libuv?

    libuv — это библиотека на языке C, которая обеспечивает механизм цикла событий Node, асинхронный ввод/вывод и пул потоков, скрывая различия в механизмах уведомления о событиях в Linux, macOS и Windows.

    Вопрос 30. setTimeout versus setInterval

    setTimeout(fn, delay) запускает функцию fn один раз через не менее чем delay миллисекунд. setInterval(fn, delay) запускает ее повторно с этой периодичностью до тех пор, пока не будет вызвана функция clearInterval().

    Вопрос 31. Обработка непойманных исключений и неразрешенных отказов

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

    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() непосредственно в коде.

    Вопрос 32. Что такое утечка памяти и как её обнаружить?

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

    Вопрос 33. __dirname против __filename

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

    Вопрос 34. Зачем использовать модуль path?

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

    Вопрос 35. fs.readFile против fs.createReadStream

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

    Основы Express

    Вопрос 36. Что такое Express?

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

    Вопрос 37. Простой сервер 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'));
    

    Вопрос 38. Что такое мидлвард в Express?

    Мидлвард в Express — это функция, принимающая параметры req, res и next. Она может выполнять любой код, изменять запрос или ответ, завершать обработку путем отправки ответа или вызывать next(), чтобы передать управление следующей функции. Классическим примером является логгер запросов:

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

    Если мидлвард не отправляет ответ и не вызывает next(), запрос застревает.

    Вопрос 39. Виды мидлвардов в Express

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

    Маршрут связывает метод 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 */ });
    

    В41. Что такое 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'));
    

    В42. app.use() против app.get() и подобных функций

    app.use() совпадает со всеми методами HTTP и любыми путями, начинающимися с указанного префикса, именно поэтому он используется для подключения промежуточных компонентов и маршрутизаторов. app.get(), app.post() и подобные методы совпадают с одним конкретным методом и полным шаблоном маршрута.

    Вопрос 43. 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().

    Вопрос 44. Разбор JSON и данных формы

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

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

    Вопрос 45. Что такое express.static()?

    Это встроенный мидлвэр, который обслуживает файлы такие как изображения, шрифтовые таблицы, скрипты и HTML из определенной директории:

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

    Промежуточный уровень Express

    Вопрос 46. Обработка ошибок

    Ошибки обрабатываются с помощью мидлвэра с четырьмя параметрами, зарегистрированного после всех маршрутов:

    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 или внутренних сообщений клиентам.

    Вопрос 47. Обработка ошибок 404

    У Express нет встроенной поддержки ошибок 404 в формате JSON. Добавьте универсальный мидлвэр после каждого маршрута; если запрос доходит до него, это означает, что ни один другой маршрут не совпал:

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

    Вопрос 48. 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
    

    В примере показаны два варианта: разрешение для всех источников или только для одного; используйте либо один, либо другой, но не оба, поскольку более permissive настройка будет применяться в первую очередь.

    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.

    В52. Загрузка файлов

    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 });
    });
    

    В реальных системах также необходимо ограничивать размер и тип файлов, а также никогда не доверять имени файла, предоставленному клиентом.

    В53. Подключение к 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 позволяет клиентам устанавливать любые поля схемы, поэтому сначала необходимо проверить данные или выбрать допустимые поля.

    В54. Порядок выполнения мидлвэров

    Express выполняет мидлвэры в том порядке, в котором они зарегистрированы, сверху вниз, причем выполнение останавливается при первой функции, которая не вызывает next(). Именно поэтому обработчики ошибок 404 и другие обработчики ошибок находятся в конце.

    В55. Механизмы шаблонов

    Механизмы шаблонов генерируют HTML на основе данных с серверной стороны. Популярными вариантами являются EJS, Pug (ранее Jade) и Handlebars. Чтобы их настроить, достаточно указать параметр движка шаблонов и вызвать функцию res.render:

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

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

    В56. Асинхронные ошибки в 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 (запуск приложения) также облегчает его импорт в тестах.

    Вопрос 58. Ограничение частоты запросов

    express-rate-limit подсчитывает количество запросов от каждого клиента в определённом временном интервале. Импортируйте его:

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

    и примените ограничитель — здесь 100 запросов с одного IP каждые 15 минут:

    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 клиента.

    Вопрос 59. Защита приложения Express

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

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

    Вопрос 61. Что делает функция next('route')?

    Он пропускает оставшиеся обработчики текущего маршрута и переходит к следующему соответствующему маршруту. Это касается только обработчиков, зарегистрированных с помощью app.METHOD() или router.METHOD() и имеющих несколько обработчиков.

    Вопрос 62. Улучшение производительности

    • Запускайте по одному процессу на ядро с использованием клустеризации или PM2.
    • Храните ответы в Redis или в оперативной памяти.
    • Сжимайте ответы с помощью промежуточного компонента compression или пусть это делает прокси.
    • Разместите перед приложением Nginx или другой обратный прокси/балансировщик нагрузки.
    • Избегайте выполнения задач, требующих больших вычислительных ресурсов CPU, в рамках цикла событий.
    • Используйте пул подключений к базе данных.
    • Рассмотрите возможность использования HTTP/2.
    • Используйте асинхронный ввод/вывод повсеместно и избегайте версий Sync в путях запросов.

    Вопрос 63. REST против GraphQL с Express

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

    Q64. Как Express обрабатывает одновременные запросы?

    Каждый запрос представляет собой лишь серию обратных вызовов в цикле событий. Поскольку операции с базой данных и сетью являются неблокирующими, один поток может обрабатывать множество запросов одновременно: сетевой трафик передается на асинхронные механизмы операционной системы, работа с файлами — на пул потоков libuv, а обратные вызовы выполняются по мере поступления результатов. В отличие от классических многопоточных серверов, здесь нет отдельного потока на каждый запрос, и именно поэтому медленная синхронная обработка влияет на всех.

    Вопрос 65. PM2 против модуля cluster

    cluster — это встроенный инструмент для масштабирования с использованием нескольких процессов. PM2 — это менеджер процессов, основанный на той же идее, но с дополнительными функциями: автоматическое перезапуск после сбоев, балансировка нагрузки, обработка логов, безперебойная замена кода и мониторинг, поэтому он широко используется в производственных средах.

    Вопрос 66. Зачем использовать обратный прокси перед Node?

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

    Вопрос 67. Конфигурация, специфичная для среды

    Пакет dotenv загружает переменные из файла .env в process.env, и приложение переходит к значениям по умолчанию, если какое-либо значение отсутствует:

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

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

    Вопрос 68. app.all() против app.use()

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

    В69. Пагинация с 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 снижается по мере увеличения смещения, в то время как пагинация с использованием курсора на индексированном поле работает эффективнее.

    В70. Что такое идемпотентность?

    Операция считается идемпотентной, если её повторение оставляет сервер в том же состоянии, что и однократное выполнение. Методы GET, PUT и DELETE определены как идемпотентные; метод POST обычно им не является, поскольку каждый вызов может создать новый ресурс. Повторные попытки после истечения времени ожидания безопасны только для идемпотентных запросов, поэтому API платежей часто добавляют ключи идемпотентности к запросам типа POST.

    Основные выводы

    • Большинство вопросов, связанных с Node, сводятся к одной модели: единственная JavaScript-нить выполнения, цикл событий с определёнными фазами, микозадачи, обрабатываемые после каждого кallback-функции, и libuv, отвечающий за остальное.
    • Большинство вопросов, связанных с Express, сводятся к другой модели: упорядоченная цепь промежуточных компонентов, где каждая функция либо отвечает, либо вызывает next(), а ошибки передаются обработчикам с четырьмя аргументами.
  • Специалисты, проводящие собеседования, ценят точность при описании изменений со временем, таких как cluster.isPrimary, ситуации с сбоями при непреобразованных ошибках, асинхронные ошибки в Express 5 и помощники для работы с путями в ESM.
  • К каждому ответу следует добавить замечание о практических рисках в производственной среде (хранение секретов в конфигурации, распространение ошибок, общие хранилища для ограничения частоты запросов), поскольку это обычно является следующим вопросом.