Вопросы и ответы с собеседований по Express.js
Вопросы и ответы с собеседований Express.js-разработчиков. Что такое middleware, для чего нужен app.use(), что такое next(), как включить CORS, как обрабатывать ошибки, для чего нужен rate limiting, способы аутентификации и обеспечения безопасности, и многое другое.
Когда полезен Express.js?
Node.js особенно полезен при создании веб-приложений, где скорость и простота имеют первостепенное значение, а задача производить тяжеловесные вычисления не стоит. Вся прелесть в том, что Node.js использует неблокирующие ввод/вывод операции, Например, обработчик события, запускаемый в момент инициации события, может установить соединения с базой данных и назначить коллбек-функцию, которая должна выполниться, когда из БД будут получены данные. Node.js не ставит всё на паузу в ожидании результата от БД, а продолжает выполнять другие операции. Когда данные будут готовы, назначенный для их обработки коллбек будет выполнен. То, что Node.js не заблокировал все происходящие процессы ради ожидания выполнения функции, и является принципом неблокирующих операций. Сам Express.js лежит в основе многих других фреймворков для Node.js. Он славится своей стабильностью, большим сообществом и массой Open Source наработок. Однако, если вам действительно важна высокая производительность, не ошибкой было бы рассмотреть другие фреймворки.
Что такое middleware?
Middleware - это функция, выполняющаяся непосредственно перед тем, как выполнение перейдет к основному обработчику события. В Express.js это применяется для добавления специальной логики в цепочку обработки запроса. Middleware может модифицировать объекты запроса и ответа, выполнять асинхронные операции и передавать управление следующему Middleware или обработчику маршрута.
Что такое app.use()?
app.use() - это функция, позволяющая вмонтировать middleware в цепочку обработки запроса.
Вмонтированные middleware вызываются в порядке их добавления, что позволяет выполнять операции последовательно.
app.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})
Пример кода выше осуществляет логирование для каждого запроса к серверу, после чего с помощью next() передает управление следующему middleware или, если таковой отсутствует, непосредственно обработчику события.
Что такое next()?
Функция next() используется в теле функции, вмонтированной в качестве middleware, для того, чтобы передать управление следующему middleware или, если таковой отсутствует, непосредственно обработчику события.
Как включить CORS?
Cross-Origin Resource Sharing (CORS) — это контролируемый и применяемый в принудительном порядке клиентом (браузером) механизм обеспечения безопасности на основе HTTP. Он позволяет службе (API) указывать любой источник (origin) помимо себя, с которого клиент может запрашивать ресурсы.
Если говорить проще, включение CORS на бэкенде позволит выполнять к нему HTTP-запросы не только с того же домена, к которому он привязан, но и с других. Каких именно - зависит от конкретной конфигурации, которую определяет сам разработчик.
Чтобы включить CORS в Express.js , необходимо использовать middleware под названием cors.
import cors from 'cors'
app.use(cors())
Это позволит выполнять запросы из любого источника. Для продвинутого контроля вы можете настроить CORS, передав объект options основной функции.
Как следует обрабатывать ошибки?
Обработка ошибок в Express.js, как и многое другое, устроено через middleware.
После всех маршрутов, для которых определяются обработчики, а также после всех иных middleware, нужно зарегистрировать middleware, который будет отвечать за обработку ошибок.
app.use(/*...*/)
app.get(/*...*/)
app.post(/*...*/)
app.put(/*...*/)
app.delete(/*...*/)
// middleware для обработки ошибок
app.use((error, req, res, next) => { /* ... */ })
Лучшие практики, которые можно привести по этой теме:
1. Централизация обработки ошибок: подразумевает создание централизованного обработчика, обрабатывающего все ошибки, которые могут возникнуть в приложении.
2. Использование HTTP статус-кодов: каждый ответ сервера должен иметь релевантный статус-код. Например, для ошибки сервера лучше отдавать статус 500, чтобы клиент мог среагировать на это соответствующим образом.
3. Логирование ошибок: все ошибки должны быть записаны в файл или отправлены специальный сервис, собирающий логи. Это позволит держать ситуацию под контролем и положительно повлияет на стабильность и надежность приложения.
4. Скрытие стек-трейсов: следует избегать прокидывания стек-трейсов клиентским приложениям - это риск для безопасности приложения.
5. Завершение работы и перезапуск: Если процесс сталкивается с необработанным исключением, необходимо обеспечить корректное завершение работы и перезапуск службы.
Как обеспечить безопасность в приложении?
Обеспечение безопасности в Express.js включает в себя несколько этапов.
Во-первых, используйте Helmet.js для безопасной установки HTTP-заголовков и предотвращения распространенных веб-уязвимостей.
Во-вторых, реализуйте rate limiting с помощью таких модулей, как express-rate-limit, для защиты от атак методом перебора.
В-третьих, проверяйте все входящие данные с помощью библиотеки, такой как express-validator, чтобы избежать атак с использованием инъекций.
В-четвертых, используйте csurf для защиты от CSRF.
В-пятых, обеспечьте безопасность файлов cookie, установив флаг "secure" и атрибуты ‘HttpOnly’.
В-шестых, конфигурируйте middleware CORS для ограничения запросов с доменов, не относящихся к приложению.
Наконец, обновляйте зависимости, чтобы снизить риски, связанные с известными уязвимостями в сторонних пакетах.
Как реализовать rate limiting?
Один из вариантов - это использование express-rate-limit.
let limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100 // не более 100 запросов в windowMs на каждый IP
});
app.use("/api/", limiter);
Такое решение ограничивает количество запросов от конкретного пользователя (по IP) в интервале 15 минут до 100. В случае превышения будет отдан HTTP-статус 429 (Too Many Requests).
Как реализовать аутентификацию?
Аутентификация в Express.js может быть реализована с помощью middleware Passport.js.
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
});
});
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function(err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
if (!user.verifyPassword(password)) { return done(null, false); }
return done(null, user);
});
}
));
app.use(passport.authenticate())
Все, что осталось:
app.use(passport.authenticate())
Какие есть способы отладки приложения?
Одним из распространенных методов отладки в Express.js является использование встроенного отладчика в Node.js , который позволяет пошагово просматривать код и проверять переменные.
Другим популярным инструментом является модуль debug, который предоставляет простой доступ к отладочной информации. Он гибкий и поддерживает пространства имен для разных частей приложения.
Middleware, такие как Morgan или Winston, также используются для регистрации HTTP-запросов и ошибок соответственно. Они предоставляют подробные журналы о входящих запросах и ответах сервера, помогая выявлять проблемы.
Для более продвинутой отладки можно использовать такие инструменты, как node-inspector или встроенный отладчик Visual Studio Code. Они предлагают графический интерфейс для установки точек останова, пошагового выполнения кода и проверки переменных.
Как обеспечить версионирование API?
Версионирование API может быть реализовано в Express.js несколькими методами.
Популярный подход - это URL-версионирование, когда разные версии API расположены по соответствующим им адресам (например, ‘/v1/users’, ‘/v2/users’). Этот метод прост и легок в реализации.
Другая стратегия заключается в использовании специальных заголовков запроса. В этом случае клиент определяет необходимую версию API установкой специального заголовка, например ‘Accept-version: v1’.
Третий вариант известен как content negotiation (согласование контента). Он заключается в том, что клиент указывает необходимую версию в заголовке 'Accept'. Например, ‘Accept: application/vnd.myapi.v1+json’.
В чём разница между res.send() и res.json()?
res.send() используется для отправки ответа с любым типом данных (строка, объект, буфер и т.д.).
res.json() используется для отправки ответа в формате JSON (автоматически устанавливает заголовок Content-Type: application/json).