Express — веб-фреймворк для Node.js
Express — самый популярный фреймворк для Node.js. Он превращает ручную возню с модулем http в аккуратную систему маршрутов, middleware и обработки ошибок. В этом курсе ты создашь полноценное веб-приложение с REST API.
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(); // передаём управление дальше
});2. Установка и первый запуск
Проект создаётся в отдельной папке установкой пакета express через npm. Для автоперезагрузки при изменениях используй пакет nodemon.
mkdir my-app
cd my-app
npm init -y
npm install express
npm install -D nodemonimport 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.
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 | удалить ресурс |
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);
}
);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 должен совпадать с парсером.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 ejsapp.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>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: "Внутренняя ошибка сервера" });
});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 | ошибка на сервере |
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);process.env. Тогда приложение легко переносить между разработкой, тестом и продакшеном.11. Подключение базы данных
Для хранения данных вместо массива подключают СУБД. Проще всего начать с SQLite — файловая база, не требует сервера. Модуль better-sqlite3 синхронный и быстрый:
npm install better-sqlite3import 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.
pg или mysql2, а для крупных проектов — ORM Prisma или квери-билдер, они облегчают миграции..env: DATABASE_URL=postgres://user:pass@host:5432/db. Пароль, зашитый в код и попавший в Git, — путь к утечке.12. Безопасность Express-приложения
Express ничего не защищает по умолчанию — безопасность собирают из middleware-пакетов:
npm install helmet cors express-rate-limitimport 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 }));| Пакет | Что защищает |
|---|---|
| helmet | HTTP-заголовки: CSP, X-Frame-Options, X-Content-Type-Options |
| cors | ограничение доменов, которым разрешены запросы |
| express-rate-limit | лимит запросов: защита от перебора и атак |
<%= … %>, а не <%- … %>.*. Для работы с cookie через fetch добавь credentials: true с обеих сторон.13. Аутентификация: сессии и JWT
Два популярных подхода для авторизации: серверные сессии с cookie и бессерверные JWT-токены. Для классических сайтов удобны сессии:
npm install express-sessionimport 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 ….
- Session — данные на сервере, проще отозвать, для нескольких инстанций нужен общий store;
- JWT — данные в самом токене, живёт до истечения срока, не хранится на сервере.
httpOnly: тогда JavaScript страницы не сможет их прочитать, и XSS-скрипту будет сложнее украсть сессию.bcrypt.hash / compare) — никогда в открытом виде или в MD5.14. Деплой и переменные окружения
Перед публикацией подготовь приложение к продакшену. Начни с пакета dotenv и явных переменных окружения:
npm install dotenv pm2import "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 # запомнить процессы для автозапуска- ставь приложение за Nginx или Caddy — TLS и раздача статики;
- раздавай
public/через веб-сервер, а не через Express; - для контейнеров используй Docker со сборкой в несколько стадий.
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, иначе браузер заблокирует соединение.17. Оптимизация и кэширование
Express-приложение ускоряется без смены стека. Стартовый набор — сжатие ответов, ETag и кэш-заголовки для статики:
npm install compression
app.use(compression());
app.set("etag", "strong");
app.use(express.static("public", { maxAge: "7d", immutable: true }));compressionсжимает JSON и HTML в gzip/br — трафик уменьшается в разы;- ETag и
Cache-Controlэкономят повторные загрузки; - запросы к базе пагинируй и прикрывай кэшем (Redis или кэш в памяти);
- тяжёлые CPU-задачи выноси из маршрута в очередь воркеров (bullmq), а не блокируй event-цикл.
Перед оптимизацией замеряй: autocannon или wrk покажут, что именно тормозит — база, парсинг или сеть. Меняй реальное узкое место, а не всё подряд.
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.
.env, обработчик ошибок — последний.Что дальше
Ты изучил ядро Express: маршруты, middleware, работу с телом запроса, шаблоны, ошибки, REST API и организацию проекта. Дальше стоит углубиться в саму платформу — повтори базу в курсе Node.js, разберись с обменом данными в HTTP и DNS и почитай про деплой в Хостинг и деплой.