Семь заданий по программированию на Node.js и Express для начинающих, с решениями и пояснениями
Пройдите семь типичных заданий на собеседованиях для начинающих разработчиков на Node.js: от маршрутов Express и промежуточных компонентов до обработки ошибок, перестановки строк и выявления дубликатов, и научитесь объяснять суть каждого из них.
На собеседованиях на должности Junior Node.js и Express редко задаются сложные алгоритмические задачи. Обычно предлагаются простые задания, такие как создание маршрута, написание middleware или очистка массива, при этом собеседник наблюдает за тем, как вы мыслите и можете ли вы объяснить, почему ваш код работает. Семь заданий ниже охватывают наиболее распространенные темы; к каждому из них приведено рабочее решение, пошаговое объяснение ключевых строк кода и вопросы для проверки настоящего понимания.
Задание 1: Создать базовый маршрут GET
Классическая разминка: «Создать GET-эндпоинт по адресу /, который возвращает сообщение Hello World.» Решение заключается в создании приложения Express, регистрации обработчика для корневого пути и начале прослушивания порта.
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.status(200).send("Hello World");
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
Поскольку используется import, проект требует наличия параметра "type": "module" в файле package.json или расширения .mjs.
Чтение строки маршрута по строкам
Вызов app.get() регистрирует обработчик, который реагирует только на запросы типа GET:
app.get("/", (req, res) => {
Первый аргумент, /, — это путь. В функции-обратном вызове передаются параметры req, описывающий входящий запрос, и res, используемый для формирования ответа.
Сам ответ генерируется здесь:
res.status(200).send("Hello World");
status(200) устанавливает код состояния (200 — это стандартное значение, но явное указание делает код более понятным), а send() записывает тело ответа и завершает его.
Наконец, запускается сервер:
app.listen(3000, () => {
Это привязывает приложение к порту 3000; обратный вызов срабатывает, когда сервер принимает подключения. В реальных сервисах порт обычно берется из process.env.PORT.
Как это сказать
"
app.get()регистрирует маршрут для запросов типа GET. Когда клиент обращается по адресу/, Express запускает обратный вызов, который отвечает со статусом 200 и текстом Hello World."
Задание 2: Написать промежуточный модуль для логирования запросов
Следующее указание: "Создайте промежуточный модуль, который будет логировать каждый входящий запрос." Промежуточный модуль — это функция с сигнатурой (req, res, next). Она выводит метод и URL запроса, а затем передает управление дальше; app.use() применяет ее ко всем зарегистрированным после нее маршрутам.
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
};
app.use(logger);
Каждый запрос проходит по цепочке промежуточных модулей в порядке, прежде чем обработчик маршрута ответит:
Client
↓
Request
↓
Logger Middleware
↓
Route Handler
↓
Response
Самая важная строка — это последняя внутри функции:
next();
next() сообщает Express, что текущий шаг завершён и следует запустить следующий мидлвэр или маршрут. Если этого не сделать, запрос застревает до истечения таймаута клиента — это частый вопрос. Альтернативой является завершение запроса путём отправки ответа, как это делает мидлвэр аутентификации при отклонении запроса из-за отсутствия токена.
Как это сформулировать
"Мидлвэр получает запрос, ответ и функцию
next. Он может изучить или изменить запрос, а затем либо ответить, либо вызватьnext(), чтобы передать управление дальше."
Задание 3: Обработка асинхронных операций в маршруте
Реальные конечные точки ожидают ответов от баз данных и других API, поэтому возникает вопрос: «Как обрабатывать асинхронные операции в Express?» Хорошим ответом будет обработчик типа async с блоками try/catch:
app.get("/users", async (req, res, next) => {
try {
const users = await getUsers();
res.status(200).json(users);
} catch (error) {
next(error);
}
});
Ключевой строкой является await:
const users = await getUsers();
Он приостанавливает выполнение этого обработчика до завершения вызова getUsers(), не блокируя при этом другие запросы. Если обещание отклоняется, выполняется блок catch:
catch (error) {
next(error);
}
next(error) передаёт ошибку в промежуточный модуль обработки ошибок из Задания 5. Стоит отметить, что в Express 4 обещания, отклонённые асинхронными обработчиками, не перехватываются, поэтому здесь обязательны блоки try/catch, тогда как в Express 5 они перехватываются автоматически. Для более подробной информации см. строительство систем обработки ошибок промышленного уровня в приложениях Node.js.
Задание 4: Нарисовать концы точек CRUD
CRUD — это аббревиатура от Create, Read, Update и Delete, четыре основные операции с данными, каждая из которых соответствует определенному методу HTTP. Примеры стабов для ресурса users:
app.post("/users", (req, res) => {
res.status(201).send("User created");
});
app.get("/users", (req, res) => {
res.status(200).send("Users");
});
app.put("/users/:id", (req, res) => {
res.status(200).send("User updated");
});
app.delete("/users/:id", (req, res) => {
res.status(200).send("User deleted");
});
При создании возвращается код 201 Created, а маршруты для одного пользователя принимают параметр :id, берущийся из req.params.id. Вместо того чтобы запоминать конкретные строки кода, нужно понимать смысл каждого метода:
POST → Create
GET → Read
PUT → Update
DELETE → Delete
Следует знать разницу между PUT и PATCH: по умолчанию PUT заменяет весь ресурс, тогда как PATCH применяет частичные изменения.
Как это сформулировать
"CRUD охватывает четыре основные операции с данными. В REST POST используется для создания, GET — для чтения, PUT или PATCH — для обновления, а DELETE — для удаления."
Задание 5: Централизованная обработка ошибок
Обработка ошибок часто выполняется после выполнения Задания 3. Начните с маршрута, который вызывает ошибку, и перенаправьте ее дальше:
app.get("/", (req, res, next) => {
try {
throw new Error("Something went wrong");
} catch (error) {
next(error);
}
});
Затем добавьте обработчик ошибок, который будет записывать логи и возвращать стандартный код 500:
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).json({
message: "Internal Server Error"
});
});
То, что делает эту функцию особенной, — это её сигнатура:
(err, req, res, next)
Express распознаёт обработчики ошибок по четырём объявленным параметрам в порядке err, req, res, next; они выполняются только тогда, когда передаётся ошибка. Зарегистрируйте обработчик после настроек маршрутов и в режиме производства избегайте отправки err.message клиентам. Порядок действий:
Route
↓
Error occurs
↓
catch(error)
↓
next(error)
↓
Error middleware
↓
500 Response
Необходимо ли использовать next() внутри обработчика ошибок?
Не нужно, если обработчик сам отправляет ответ:
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).send("Internal Server Error");
});
next всё равно должен быть объявлен, потому что при наличии трёх параметров Express считает функцию обычным промежуточным компонентом.
Задание 6: Инвертировать строку
Некоторые задачи решаются с помощью обычного JavaScript: «Переверните строку hello.» Типичное однострочное решение объединяет три метода:
const str = "hello";
const reversed = str.split("").reverse().join("");
console.log(reversed);
Оно выводит:
olleh
Что делает каждый шаг
Метод split преобразует строку в массив символов:
str.split("")
Результатом является массив:
["h", "e", "l", "l", "o"]
Затем порядок элементов в массиве меняется на месте:
.reverse()
В результате получается:
["o", "l", "l", "e", "h"]
Наконец, символы объединяются без разделителей:
.join("")
Это даёт перевернутую строку:
"olleh"
Распространённая модификация задачи: «Теперь сделайте это без использования reverse().» Необходимо пройтись по элементам массива от последнего индекса до нуля:
const str = "hello";
let reversed = "";
for (let i = str.length - 1; i >= 0; i--) {
reversed += str[i];
}
console.log(reversed);
Оба подхода имеют линейную сложность. Стоит отметить, что методы split("") и индексация работают с кодовыми единицами UTF-16, поэтому эмодзи разделяются на отдельные символы; в то время как [...str] разделяет строку по кодовым точкам.
Задание 7: Найти дубликаты в массиве
const numbers = [1, 2, 3, 2, 4, 3, 5];
[2, 3]
Set: один для уже увиденных значений, другой — для подтверждённых дубликатов:
const numbers = [1, 2, 3, 2, 4, 3, 5];
const seen = new Set();
const duplicates = new Set();
for (const number of numbers) {
if (seen.has(number)) {
duplicates.add(number);
} else {
seen.add(number);
}
}
console.log([...duplicates]);
[2, 3]
Почему работают два набора
Набор seen фиксирует каждое пройденное значение. При первом вхождении оно добавляется туда:
seen.add(number);
if (seen.has(number))
duplicates, и поскольку набор игнорирует повторы, о нём сообщается только один раз. Поиск в наборе выполняется практически мгновенно, поэтому решение имеет линейную сложность, а не квадратичную из-за вложенных циклов. Чтобы подсчитать количество вхождений, используйте Map.
Обсудите свой подход перед тем, как писать код
Самая важная привычка — объяснять свой план перед написанием кода, поскольку собеседники оценивают логику не меньше, чем результаты. Для задачи на выявление дубликатов начните с чего-то вроде следующего:
«План заключается в использовании множества уже виденных значений. Во время цикла любое значение, уже присутствующее в этом множестве, попадает во второе множество дубликатов, что обеспечивает уникальность каждого из них».
Затем пишите код. Это доказывает понимание материала, а не просто заучивание, и позволяет собеседнику заранее направить вас по правильному пути.
Чек-лист подготовки
Перед собеседованием на должность junior Node.js или Express убедитесь, что вы можете с легкостью:
- Определять маршруты и объяснять функции
reqиres - Использовать
app.use()и писать собственные промежуточные компоненты - Объяснять, что делает функция
next()и что происходит, если она отсутствует
async/await, try/catch и next(error)Set и Map для поиска и подсчёта элементовНа этом уровне прочные основы, читаемый код и ясные объяснения имеют гораздо большее значение, чем сложные алгоритмы.
Связанная литература
- Node.js 26: Temporal API, Map Upserts и Undici 8 — подробное объяснение — Рассматриваются основные изменения в Node.js 26, касающиеся бэкенда: стабильный Temporal API, встроенные методы обновления элементов в Map, улучшения производительности Undici 8, а также изменения, которые могут привести к сбоям при обновлении.
- Node.js Streams: как избежать сбоев сервера из-за нехватки памяти — Объясняется, почему загрузка целых файлов в память приводит к сбоям сервера Node.js, и как потоки типов readable, writable, duplex и transform решают эту проблему с помощью механизма обратного давления.