Backend

Как собрать REST API на Express.js за вечер

Владислав Анохин
Владислав Анохин21 окт. 2026
Обложка со схемой потока: логотип Express.js, серверный куб с логотипом Node.js, карточки ответов на оранжевом маршруте и окно браузера

Привет! REST API на Express.js правда собирается за вечер. Фреймворк почти ничего не навязывает: даёт минимум для старта, а остальное вы подключаете сами. express() создаёт приложение, и это приложение - просто очередь middleware, которые выполняются одна за другой.

Ниже пройдём путь от пустого проекта до ответа 201 из PostgreSQL. Если после Express захочется жёсткой структуры и готовых слоёв, загляните в разбор чем Nest.js отличается от Express и когда его берут. К концу статьи у вас будет работающий сервер, роутер, обработчик ошибок и запросы в базу.

Что такое Express.js и что понадобится для старта

Express - минималистичный веб-фреймворк для Node.js. Из коробки он даёт маршрутизацию, middleware и парсинг JSON-тел через express.json(). Логирование, база и всё остальное - отдельными пакетами.

Актуальная версия - Express 5.3.0, ей нужен Node.js 18 или выше. Флаги из примеров ниже (--watch, --env-file) есть не во всех версиях 18+: --watch появился в 18.11.0, --env-file - только с 20.6.0. Сначала проверьте окружение и поставьте зависимости:

node --version
npm init -y
npm i express pg
npm ls express

Первая команда показывает версию Node.js: актуальная LTS-линия сейчас - 24 (Krypton). Дальше npm init -y создаёт package.json без вопросов, следом ставим Express и клиент PostgreSQL pg. Последняя команда подтверждает, что встала именно пятёрка.

Минимальный сервер, который уже отвечает

Сервер собирается из трёх шагов: создать приложение, объявить маршрут, вызвать app.listen. Этого хватает, чтобы порт начал слушаться. Пример отдельный, в одном файле src/server.js, дальше соберём проект из нескольких.

// src/server.js
import express from 'express';

const app = express();

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(3000, (error) => {
  if (error) throw error;
  console.log('http://localhost:3000');
});

app - это функция-обработчик запроса, поэтому её можно передать в http.createServer. Вызов app.listen поднимает HTTP-сервер на порту 3000 и возвращает объект этого сервера. Колбэк получает ошибку: если порт занят, придёт EADDRINUSE, и вы узнаете об этом сразу, а не по молчаливому зависанию.

Перезапускать процесс при правках удобно встроенным node --watch src/server.js. Флаг стабилен с Node.js 22.0.0, в ветке 20 - с 20.13.0, так что отдельный nodemon не нужен.

Маршрутизация, параметры и роутеры

Маршрут в Express - это HTTP-метод, путь и функция-обработчик. Параметры объявляют двоеточием в пути (:id) и читают из req.params. Полный синтаксис путей описан в гайде по маршрутизации Express.

Роутер раскладывает маршруты по файлам, чтобы app.js не превратился в свалку. Код ниже живёт в каталоге src. Точка входа - src/server.js (импортирует приложение и вызывает app.listen), сборка приложения - src/app.js, рядом src/db.js и src/routes/users.js. Начнём с роутера:

// routes/users.js
import express from 'express';
import { pool } from '../db.js';

const router = express.Router();

router.get('/', async (req, res) => {
  const result = await pool.query('SELECT id, email FROM users ORDER BY id');
  res.json(result.rows);
});

router.get('/:id', async (req, res) => {
  const result = await pool.query('SELECT id, email FROM users WHERE id = $1', [req.params.id]);
  const [user] = result.rows;
  if (!user) {
    return res.status(404).json({ error: 'Not Found' });
  }
  res.json(user);
});

router.post('/', async (req, res) => {
  const { email } = req.body ?? {};
  if (!email) {
    return res.status(400).json({ error: 'email is required' });
  }
  const result = await pool.query(
    'INSERT INTO users (email) VALUES ($1) RETURNING id, email',
    [email]
  );
  res.status(201).json(result.rows[0]);
});

export default router;

Внутри три обработчика: список пользователей, один пользователь и создание. Первым аргументом pool.query идёт SQL, вторым - массив значений для $1, а сами строки лежат в result.rows. Создание отвечает кодом 201 Created и возвращает вставленную запись.

Осталось смонтировать роутер в приложении:

// app.js
import express from 'express';
import usersRouter from './routes/users.js';

const app = express();
app.use(express.json());
app.use('/api/users', usersRouter);

export default app;

express.Router() создаёт объект с тем же набором методов, что у приложения, а app.use('/api/users', usersRouter) добавляет префикс: GET / внутри файла превращается в GET /api/users. Встроенный express.json() парсит только тела с Content-Type: application/json и по умолчанию принимает не больше 100kb.

С req.body в Express 5 есть ловушка: если тело не распарсилось, он равен undefined, а не пустому объекту. Пришлёте text/plain - обращение к req.body.email упадёт с TypeError и превратится в 500. Поэтому проверяйте тело до чтения полей, как в обработчике выше через req.body ?? {}.

Чем маршруты в Express 5 отличаются от Express 4

Express 5 перешёл на path-to-regexp v8, и часть привычных записей перестала работать. Wildcard теперь обязан быть именованным: вместо /* пишут /*splat. Символ ? в пути больше не поддерживается, вместо него опциональную часть задают фигурными скобками. Регулярные выражения внутри строкового пути тоже запрещены.

Заодно из пятёрки убрали старые сигнатуры вроде res.send(status) и app.del(). Если переносите код с четвёрки, начните с этих вызовов: Express 5 вышел в 2024 году, и они перестанут работать.

Зачем нужен middleware и почему порядок важнее содержимого

Middleware - это функция, через которую проходит каждый запрос: на входе req, res и next. Она либо отвечает клиенту, либо передаёт управление дальше через next().

Свой логгер занимает четыре строки:

export function requestLogger(req, res, next) {
  console.log(`${req.method} ${req.originalUrl}`);
  next();
}

// app.use(requestLogger) - до маршрутов

Ничего лишнего: пишем метод и путь, потом вызываем next(). Подключать логгер нужно до маршрутов. Middleware, объявленный после обработчика, уже не сработает: ответ успел завершить цепочку.

Забыли вызвать next() и не отправили ответ - запрос висит до таймаута клиента. Это классическая поломка в самописных middleware.

Инфографика порядка middleware в Express: цепочка «запрос → requestLogger → express.json() → маршрут → ответ 200» и второй вариант, где без next() запрос висит до таймаута

Обработка ошибок и почему 404 - не ошибка

Обработчик ошибок - это middleware строго с четырьмя аргументами (err, req, res, next). Ответ 404 в него не попадает: это не ошибка, и ловить его нужно обычным middleware в самом низу стека, после всех маршрутов. Подробности есть в гайде Express по обработке ошибок.

// 404 - не ошибка, ловим последним обычным middleware
app.use((req, res) => {
  res.status(404).json({ error: 'Not Found' });
});

// обработчик ошибок: обязательно 4 аргумента
app.use((err, req, res, next) => {
  if (res.headersSent) {
    return next(err);
  }
  console.error(err);
  res.status(err.status ?? 500).json({ error: err.message });
});

Держится всё на порядке блоков. Уберите четвёртый аргумент - Express решит, что перед ним обычный middleware, и ошибки до него не дойдут. Если ответ уже начал отправляться (res.headersSent), правильнее вернуть ошибку встроенному обработчику через next(err).

В Express 5 отклонённый Promise из async-обработчика уходит в error-handler сам, try/catch вокруг await нужен только для локальной обработки. В четвёрке такой автоматики не было, ошибку передавали вручную через .catch(next).

express-error-handler-flow.webp

Подключение к PostgreSQL через пул

С PostgreSQL в Node.js работают через библиотеку pg. Пул держат один на процесс, строку подключения берут из process.env, запросы идут через pool.query с параметрами $1. Пул переиспользует соединения вместо открытия нового на каждый запрос.

// db.js
import pg from 'pg';

const { Pool } = pg;

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export async function closePool() {
  await pool.end();
}

new Pool читает строку подключения из переменной DATABASE_URL. По умолчанию пул держит до 10 клиентов, освобождает простаивающие через 10 секунд и не ставит таймаут на подключение. Для локальной разработки этого хватает. pool.end() закрывает соединения, его вызывают при остановке процесса.

Строку подключения держим в .env, например DATABASE_URL=postgresql://user:password@localhost:5432/app. Отдельный dotenv не нужен: Node.js читает файл сам, флаг --env-file появился в 20.6.0 и перестал быть экспериментальным в 24.10. Это описано в разделе про `--env-file` в документации CLI Node.js.

{
  "type": "module",
  "scripts": {
    "dev": "node --watch --env-file=.env src/server.js",
    "start": "node --env-file=.env src/server.js"
  }
}

"type": "module" включает ESM, то есть штатный синтаксис модулей, чтобы работали import из примеров выше. Скрипт dev перезапускает сервер при изменениях, start запускает его без вотчера, оба подхватывают .env.

Ещё не решили, какая база будет под проектом? Сравнение PostgreSQL или MySQL - какую СУБД выбрать поможет определиться до того, как код обрастёт запросами.

Что дальше после первого API

Первый API готов, дальше обычно расходятся по трём направлениям: разложить код по слоям, добавить реальное время, проверить базу под нагрузкой.

  • Когда маршрутов и бизнес-логики становится много, смотрят в сторону Nest.js: там модули и внедрение зависимостей заданы фреймворком.
  • Если нужны пуш-обновления вместо схемы «запрос-ответ», читайте как работает реальное время на WebSocket.
  • Базу под нагрузкой проверяют отдельно: пул соединений, индексы, медленные запросы.

Коротко: приложение - это очередь middleware, роутер раскладывает маршруты по файлам, обработчик ошибок стоит последним, пул pg переиспользует соединения. Такого набора хватает, чтобы поднять CRUD-сервис и спокойно наращивать его дальше.

📩 Собираете API или сервис на Node.js? Расскажите о задаче - предложим решение: оставить заявку.

Читайте также

WebSockets: протокол двусторонней связи в реальном времени между браузером и сервером - изометрическая иллюстрация с потоком данных
Backend

WebSocket: как работает реальное время в вебе

WebSocket - протокол связи, который позволяет серверу и клиенту обмениваться данными в реальном времени через одно постоянное соединение. В статье разбираем устройство протокола, сравниваем с HTTP и реализуем практические примеры на Node.js: echo-сервер, broadcast и чат с rooms через Socket.IO.

Владислав Анохин
Владислав Анохин
24 апр. 2026
NestJS
Backend

Nest.js: современный backend-фреймворк для Node.js

Nest.js — современный backend-фреймворк для Node.js с TypeScript. Разбираем архитектуру, модули, внедрение зависимостей и создание REST API. Практические примеры и сравнение с Express.js помогут выбрать лучший инструмент для вашего проекта. Узнайте, почему крупные компании выбирают Nest.js для масштабируемых приложений.

Владислав Анохин
Владислав Анохин
04 апр. 2026