Главная / Статьи / Семь заданий по программированию на Node.js и Express для начинающих, с решениями и пояснениями

Семь заданий по программированию на Node.js и Express для начинающих, с решениями и пояснениями

Пройдите семь типичных заданий на собеседованиях для начинающих разработчиков на Node.js: от маршрутов Express и промежуточных компонентов до обработки ошибок, перестановки строк и выявления дубликатов, и научитесь объяснять суть каждого из них.

1640 слов

На собеседованиях на должности 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)
  • Напишите обработчик ошибок с четырьмя параметрами и зарегистрируйте его после маршрутов
  • Сопоставьте операции CRUD с методами HTTP и соответствующими кодами состояния
  • Манипулируйте строками и массивами с помощью основных методов JavaScript
  • Выбирайте между Set и Map для поиска и подсчёта элементов
  • Описывайте свой подход в процессе написания кода
  • На этом уровне прочные основы, читаемый код и ясные объяснения имеют гораздо большее значение, чем сложные алгоритмы.

    Связанная литература

  • Соответствие типов времени реальным задачам с датами: семь примеров с пошаговым решением — Разберём семь повседневных задач, связанных с датами и временем, с использованием библиотеки JavaScript Temporal: от простых дат и проблем с месяцами до часовых поясов, арифметики с переходом на летнее время и проверки данных.
  • Подготовка к собеседованию на Node.js и Express: 70 вопросов с точными ответами — Структурированный обзор 70 вопросов к собеседованиям по Node.js и Express, охватывающий такие темы, как цикл событий и потоки, промежуточные компоненты, безопасность и масштабирование, с исправлениями распространённых ошибочных ответов.