Express — веб-фреймворк для Node.js

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

Уровень: начальный 18 разделов 3–4 часа

1. Что такое Express и middleware

Express — минималистичный фреймворк поверх Node.js. Вместо ручного разбора req.url ты описываешь маршруты один раз, а повторяющаяся логика выносится в функции-посредники — middleware.

Middleware — это функции, которые выполняются между получением запроса и отправкой ответа. Каждая такая функция принимает объекты req, res и функцию next. Вызвал next() — передал управление следующему обработчику в цепочке.

app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} — ${new Date().toISOString()}`);
  next(); // передаём управление дальше
});
Представь конвейер: запрос проходит по цепочке обработчиков, каждый может что-то добавить в req, ответить сам или передать дальше через next(). Так приложение собирается из маленьких кирпичиков.

2. Установка и первый запуск

Проект создаётся в отдельной папке установкой пакета express через npm. Для автоперезагрузки при изменениях используй пакет nodemon.

mkdir my-app
cd my-app
npm init -y
npm install express
npm install -D nodemon
import express from "express";

const app = express();
const port = 3000;

app.get("/", (req, res) => {
  res.send("Привет от Express!");
});

app.listen(port, () => {
  console.log(`Сервер: http://localhost:${port}`);
});

Не забудь в package.json добавить поле "type": "module" для поддержки import. Запуск — командой npx nodemon app.js.

Разница с чистым Node.js видна сразу: res.send() сам определяет тип содержимого — текстовая строка станет text/plain, объект превратится в JSON, а строка с HTML будет отдана как text/html.

3. Маршруты

Маршрут — это связка «метод + путь + обработчик». Express поддерживает все HTTP-методы: app.get, app.post, app.put, app.delete и другие.

import express from "express";
const app = express();

// список товаров
app.get("/products", (req, res) => {
  res.json([{ id: 1, name: "Кроссовки" }, { id: 2, name: "Шапка" }]);
});

// добавление нового товара
app.post("/products", (req, res) => {
  res.status(201).json({ message: "Товар добавлен" });
});

// удаление
app.delete("/products", (req, res) => {
  res.json({ message: "Товар удалён" });
});

Методы HTTP

МетодНазначение
GETполучить данные (без побочных эффектов)
POSTсоздать новый ресурс
PUTполностью заменить ресурс
PATCHчастично изменить ресурс
DELETEудалить ресурс
Порядок маршрутов важен! Express проверяет их в том порядке, в котором они объявлены. Первый подходящий маршрут выигрывает, поэтому специфичные пути ставь до обобщённых.

4. Параметры пути и query

Параметры пути (:id) извлекают значения прямо из URL, а query-параметры приходят после знака вопроса.

// /users/42  →  req.params.id === "42"
app.get("/users/:id", (req, res) => {
  res.json({ userId: req.params.id });
});

// /search?q=носки&page=2  →  req.query
app.get("/search", (req, res) => {
  res.json({
    q: req.query.q,
    page: req.query.page ?? 1,
  });
});
// несколько параметров: /news/2026/12
app.get("/news/:year/:month", (req, res) => {
  res.send(`Архив ${req.params.year}-${req.params.month}`);
});
Значения из req.params и req.query приходят как строки. Для чисел используй Number() и проверяй результат, иначе на /users/abc твоё приложение получит «странные» данные.

5. Цепочки middleware

Middleware можно назначать на конкретный маршрут или на всё приложение. Порядок регистрации равен порядку выполнения.

// логгер — применяется ко всем запросам
app.use((req, res, next) => {
  console.log(`[${req.method}] ${req.originalUrl}`);
  next();
});

// middleware для одного маршрута
function isAdmin(req, res, next) {
  if (req.headers["x-admin"] !== "yes") {
    return res.status(403).json({ error: "Доступ запрещён" });
  }
  next();
}

app.get("/admin", isAdmin, (req, res) => {
  res.json({ secret: "данные админки" });
});
// цепочка из нескольких обработчиков
app.get(
  "/profile",
  checkAuth,
  loadUser,
  (req, res) => {
    res.json(req.user);
  }
);
Если middleware отправил ответ (res.send, res.json, res.status), но не вызвал next() — запрос завершается. Если не вызвать ни res-метод, ни next — запрос «зависнет» до истечения таймаута. Всегда заканчивай middleware либо ответом, либо next().

6. Парсинг тела запроса

Чтобы читать данные, присланные в POST, Express нужно подключить встроенные парсеры: express.json() для JSON и express.urlencoded() для данных форм.

import express from "express";
const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/register", (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: "name и email обязательны" });
  }
  res.status(201).json({ ok: true, user: { name, email } });
});

Проверить маршрут можно через curl или клиент API (Postman, Insomnia):

curl -X POST http://localhost:3000/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Иван","email":"ivan@mail.ru"}'
Без express.json() тело запроса будет пустым: req.body останется пустым объектом. Это самая частая причина, почему POST «не работает». Заголовок Content-Type должен совпадать с парсером.
Всегда валидируй входящие данные. Проверка name и email выше — минимальная; в реальном проекте используй библиотеку валидации вроде zod или express-validator.

7. Статика и шаблоны

Статические файлы (CSS, JS, картинки) раздаются одной строкой через express.static. Файлы из папки public становятся доступны по своим относительным путям.

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

// теперь файл public/css/style.css доступен по адресу:
// http://localhost:3000/css/style.css

Для динамических HTML-страниц используются шаблонизаторы. Один из самых простых — EJS, позволяющий вставлять переменные прямо в HTML.

npm install ejs
app.set("view engine", "ejs");

app.get("/users", async (req, res) => {
  const users = await db.getAllUsers();
  res.render("users", { users });
});
<!-- views/users.ejs -->
<h1>Список пользователей</h1>
<ul>
  <% users.forEach(u => { %>
    <li><%= u.name %> — <%= u.email %></li>
  <% }) %>
</ul>
Папку public нужно создать заранее (например, public/css/style.css). Не размещай там секретные файлы — их содержимое доступно всем посетителям сайта.

8. Обработка ошибок

Обработчик ошибок — это middleware с четырьмя параметрами (err, req, res, next). Express сам вызывает его, когда в обычном обработчике происходит исключение или вызывается next(err).

app.get("/users/:id", async (req, res, next) => {
  try {
    const user = await db.getUser(req.params.id);
    if (!user) return res.status(404).json({ error: "Не найден" });
    res.json(user);
  } catch (err) {
    next(err); // передаём ошибку в обработчик ниже
  }
});

// обработчик ошибок — идёт ПОСЛЕ всех маршрутов
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: "Внутренняя ошибка сервера" });
});
Обработчик ошибок обязан находиться после всех маршрутов и иметь ровно четыре параметра. Если в нём не вызвать next() — ошибка либо «проглотится», либо упадёт весь процесс.
В продакшене не отправляй клиенту полный стек ошибки — это раскрывает внутренности проекта. Логируй детали в файл или сервис мониторинга, а клиенту отдавай общее сообщение.

9. REST API на Express

REST — это стиль построения API, где ресурсы (товары, пользователи, заказы) адресуются через URL, а действия выражаются HTTP-методами. Соберём полноценный CRUD для ресурса «товары». Данные для простоты храним в массиве.

import express from "express";
const app = express();
app.use(express.json());

let products = [
  { id: 1, name: "Кроссовки", price: 4990 },
  { id: 2, name: "Шапка", price: 990 },
];

// GET /products — список
app.get("/products", (req, res) => res.json(products));

// GET /products/:id — один товар
app.get("/products/:id", (req, res) => {
  const p = products.find(x => x.id === Number(req.params.id));
  if (!p) return res.status(404).json({ error: "Не найден" });
  res.json(p);
});

// POST /products — создание
app.post("/products", (req, res) => {
  const { name, price } = req.body;
  if (!name || typeof price !== "number") {
    return res.status(400).json({ error: "Некорректные данные" });
  }
  const id = Math.max(0, ...products.map(p => p.id)) + 1;
  const product = { id, name, price };
  products.push(product);
  res.status(201).json(product);
});
// PUT /products/:id — полная замена
app.put("/products/:id", (req, res) => {
  const idx = products.findIndex(x => x.id === Number(req.params.id));
  if (idx === -1) return res.status(404).json({ error: "Не найден" });
  products[idx] = { id: Number(req.params.id), ...req.body };
  res.json(products[idx]);
});

// DELETE /products/:id — удаление
app.delete("/products/:id", (req, res) => {
  products = products.filter(x => x.id !== Number(req.params.id));
  res.status(204).end();
});

Типовые коды ответов

КодЗначение
200успех (для GET, PUT)
201ресурс создан (POST)
204успех без тела ответа (DELETE)
400некорректный запрос
401 / 403нет аутентификации / нет прав
404ресурс не найден
500ошибка на сервере
В реальном проекте данные хранятся не в массиве, а в базе данных. Подробнее о работе с СУБД из Node.js — в уроке Node.js, а об архитектуре API — в курсе REST API.

10. Структура проекта

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

my-app/
├── app.js              # сборка приложения (middleware, маршруты)
├── server.js           # запуск: listen() и стартовые действия
├── package.json
├── .env                # секреты и порт (не в Git!)
├── routes/
│   └── products.js     # маршруты /products
├── controllers/
│   └── products.js     # логика обработки запросов
├── db/
│   └── index.js        # подключение и запросы к базе
├── middleware/
│   └── auth.js         # проверка прав, логгер
└── public/             # статические файлы
// routes/products.js — роутер как отдельный модуль
import { Router } from "express";
import * as controller from "../controllers/products.js";

const router = Router();
router.get("/", controller.list);
router.get("/:id", controller.get);
router.post("/", controller.create);

export default router;

// в app.js подключаем роутер к префиксу
import productsRouter from "./routes/products.js";
app.use("/products", productsRouter);
Храни секреты в переменных окружения (.env) и добавь этот файл в .gitignore. Пароль от базы данных в коде, попавшем в публичный репозиторий, — повод перевыпускать все учётки.
Порты, хосты и ключи почти всегда должны задаваться через process.env. Тогда приложение легко переносить между разработкой, тестом и продакшеном.

11. Подключение базы данных

Для хранения данных вместо массива подключают СУБД. Проще всего начать с SQLite — файловая база, не требует сервера. Модуль better-sqlite3 синхронный и быстрый:

npm install better-sqlite3
import Database from "better-sqlite3";
const db = new Database("shop.db");

db.exec(`CREATE TABLE IF NOT EXISTS products (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  price INTEGER
)`);

const insert = db.prepare("INSERT INTO products (name, price) VALUES (?, ?)");
insert.run("Кроссовки", 4990);

const rows = db.prepare("SELECT * FROM products").all();

Prepared statements с плейсхолдерами ? защищают от SQL-инъекций: параметры передаются отдельно от запроса и не могут «встроиться» в SQL.

Для PostgreSQL и MySQL используй официальный драйвер pg или mysql2, а для крупных проектов — ORM Prisma или квери-билдер, они облегчают миграции.
Строку подключения храни в .env: DATABASE_URL=postgres://user:pass@host:5432/db. Пароль, зашитый в код и попавший в Git, — путь к утечке.

12. Безопасность Express-приложения

Express ничего не защищает по умолчанию — безопасность собирают из middleware-пакетов:

npm install helmet cors express-rate-limit
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";

app.use(helmet());
app.use(cors({ origin: ["https://site.ru"] }));
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
ПакетЧто защищает
helmetHTTP-заголовки: CSP, X-Frame-Options, X-Content-Type-Options
corsограничение доменов, которым разрешены запросы
express-rate-limitлимит запросов: защита от перебора и атак
Не выводи пользовательский HTML без экранирования — это XSS-инъекция. В шаблонах EJS экранирует тег <%= … %>, а не <%- … %>.
В продакшене задавай CORS явным списком доменов, а не *. Для работы с cookie через fetch добавь credentials: true с обеих сторон.

13. Аутентификация: сессии и JWT

Два популярных подхода для авторизации: серверные сессии с cookie и бессерверные JWT-токены. Для классических сайтов удобны сессии:

npm install express-session
import session from "express-session";

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { httpOnly: true, sameSite: "lax" }
}));

app.post("/login", (req, res) => {
  req.session.user = { id: 1, name: "Иван" };
  res.json({ ok: true });
});

app.get("/me", (req, res) => res.json(req.session.user ?? null));

JWT подходит для API и мобильных приложений: сервер подписывает токен, а клиент носит его в заголовке Authorization: Bearer ….

Cookie сессии ставь с httpOnly: тогда JavaScript страницы не сможет их прочитать, и XSS-скрипту будет сложнее украсть сессию.
Пароли храни только как хэш через bcrypt (bcrypt.hash / compare) — никогда в открытом виде или в MD5.

14. Деплой и переменные окружения

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

npm install dotenv pm2
import "dotenv/config";

const port = process.env.PORT || 3000;
const isProd = process.env.NODE_ENV === "production";

app.listen(port, () => {
  console.log(`Сервер на :${port} (${isProd ? "prod" : "dev"})`);
});

Менеджер процессов pm2 держит приложение живым и перезапускает его при падении:

pm2 start server.js --name my-app
pm2 save   # запомнить процессы для автозапуска
Логи в продакшене собирай в ELK или Grafana Loki — так баги в Express видны до того, как на них пожалуются пользователи.
Автоматизируй деплой: push в main → тесты → деплой через GitHub Actions или GitLab CI. Деплой руками — плохая привычка.

15. Валидация данных: zod и express-validator

Ручные проверки из раздела 6 разрастаются в сотни строк. Готовые библиотеки упорядочивают процесс: express-validator встраивается в middleware Express, а zod описывает схемы с типизацией TypeScript. С числами, булевыми и вложенными объектами вручную легко ошибиться — лучше валидировать декларативно.

npm install zod

// регистрация с декларативной схемой
const schema = z.object({
  name: z.string().min(2).max(60),
  email: z.string().email(),
  age: z.coerce.number().int().min(18).optional()
});

app.post("/register", (req, res) => {
  const parsed = schema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ errors: parsed.error.issues });
  }
  res.status(201).json({ ok: true, data: parsed.data });
});

Разница подходов: express-validator удобен точечными цепочками правил на конкретное поле, zod — переиспользуемыми схемами, которые заодно генерируют типы. В обоих случаях возвращай клиенту понятные сообщения, а не весь технический объект ошибки.

Валидируй и req.params, и req.query: строка «abc» в :id превратится в NaN через Number(), и странные значения уйдут в запрос к базе.

16. Реалтайм: WebSocket и socket.io

Для чатов, уведомлений и совместного редактирования одного HTTP мало: клиент должен получать события без перезагрузки. Подключай WebSocket — пакет ws или socket.io поверх него:

npm install socket.io

import { Server } from "socket.io";

const server = app.listen(3000);
const io = new Server(server, { cors: { origin: ["https://site.ru"] } });

io.on("connection", (socket) => {
  socket.on("message", (text) => io.emit("message", text));
});

На клиенте всё симметрично: const socket = io();, затем socket.emit("message", "Привет") и socket.on("message", ...). Обычные HTTP-маршруты Express и сокеты отлично уживаются: REST — данные, сокеты — события.

Сокеты не отменяют CORS: в socket.io он настраивается отдельной опцией cors, иначе браузер заблокирует соединение.
При росте добавь redis-adapter, чтобы события распространялись между несколькими инстансами приложения, а не только внутри одного процесса.

17. Оптимизация и кэширование

Express-приложение ускоряется без смены стека. Стартовый набор — сжатие ответов, ETag и кэш-заголовки для статики:

npm install compression

app.use(compression());
app.set("etag", "strong");
app.use(express.static("public", { maxAge: "7d", immutable: true }));

Перед оптимизацией замеряй: autocannon или wrk покажут, что именно тормозит — база, парсинг или сеть. Меняй реальное узкое место, а не всё подряд.

Добавь заголовок времени ответа и следи за p99 по ключевым маршрутам в Prometheus — так деградация видна до жалоб пользователей.

18. Тестирование API и типовые ошибки

Маршруты Express покрывают интеграционными тестами через supertest: он обращается к объекту app без listen().

npm install -D supertest vitest

// app.test.js
import request from "supertest";
import app from "./app.js";

test("GET /products/:id", async () => {
  const res = await request(app).get("/products/2");
  expect(res.status).toBe(200);
  expect(res.body.name).toBe("Шапка");
});

Для тестов используй отдельную базу или SQLite в памяти, а лимиты и секреты отключай через окружение. Типовые ошибки при этом: забытый express.json() перед POST-тестами, сравнение id как строки и обработчик ошибок, объявленный не в конце — тогда тест получит HTML вместо JSON.

Не гоняй тесты против живой базы продакшена: применяй транзакции с откатом или отдельную схему, иначе данные будут затёрты.
Чек-лист перед деплоем: body-парсер подключён, helmet/CORS/rate-limit на месте, пользовательский HTML экранирован, секреты в .env, обработчик ошибок — последний.

Что дальше

Ты изучил ядро Express: маршруты, middleware, работу с телом запроса, шаблоны, ошибки, REST API и организацию проекта. Дальше стоит углубиться в саму платформу — повтори базу в курсе Node.js, разберись с обменом данными в HTTP и DNS и почитай про деплой в Хостинг и деплой.

← Вернуться к курсам