Express.js
Express.js — минималистичный веб-фреймворк для Node.js. Он упрощает маршрутизацию, обработку HTTP-запросов и ответов, подключение middleware и создание API.
Установка:
npm init -y
npm install expressДля ES Modules добавьте в package.json:
{
"type": "module",
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js"
}
}Минимальный сервер:
import express from "express";
const app = express();
const PORT = process.env.PORT ?? 3000;
app.get("/", (req, res) => {
res.send("Hello from Express");
});
app.listen(PORT, () => {
console.log(`Server started on http://localhost:${PORT}`);
});В CommonJS импорт выглядит так:
const express = require("express");Роутинг в Express
Маршрут связывает HTTP-метод и URL с обработчиком.
app.METHOD(PATH, HANDLER);Основные методы:
| Метод | Назначение |
|---|---|
app.get() |
Получение данных |
app.post() |
Создание ресурса |
app.put() |
Полная замена ресурса |
app.patch() |
Частичное изменение |
app.delete() |
Удаление ресурса |
CRUD-пример:
app.get("/users", getUsers);
app.get("/users/:id", getUserById);
app.post("/users", createUser);
app.patch("/users/:id", updateUser);
app.delete("/users/:id", deleteUser);Параметры маршрута
Динамические части пути доступны через req.params:
app.get("/users/:id", (req, res) => {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: "Некорректный id" });
}
res.json({ id });
});Для запроса GET /users/42 значение req.params.id равно строке "42".
Query-параметры
Параметры после ? находятся в req.query:
GET /products?category=books&page=2app.get("/products", (req, res) => {
const { category, page = "1" } = req.query;
res.json({ category, page: Number(page) });
});express.Router()
Роутер объединяет связанные маршруты:
import { Router } from "express";
const router = Router();
router.get("/", getUsers);
router.get("/:id", getUserById);
router.post("/", createUser);
export default router;Подключение:
import usersRouter from "./routes/users.routes.js";
app.use("/api/users", usersRouter);Получатся адреса /api/users и /api/users/:id.
Несколько методов одного пути можно объединить:
router
.route("/:id")
.get(getUserById)
.patch(updateUser)
.delete(deleteUser);Обработчик неизвестного маршрута размещается после существующих маршрутов:
app.use((req, res) => {
res.status(404).json({ error: "Маршрут не найден" });
});Middleware
Middleware — функция, выполняемая между получением запроса и отправкой ответа.
function middleware(req, res, next) {
// Обработка запроса
next();
}Middleware может:
- читать и изменять
reqиres; - завершить запрос отправкой ответа;
- вызвать
next()и продолжить цепочку; - вызвать
next(error)и передать ошибку обработчику.
Встроенные middleware
JSON-тело запроса:
app.use(express.json({ limit: "100kb" }));Данные HTML-формы:
app.use(express.urlencoded({ extended: true }));Статические файлы:
app.use(express.static("public"));Кастомное middleware
Логирование запросов:
function requestLogger(req, res, next) {
const startedAt = Date.now();
res.on("finish", () => {
const duration = Date.now() - startedAt;
console.log(
`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`
);
});
next();
}
app.use(requestLogger);Проверка авторизации для отдельного маршрута:
function requireAuth(req, res, next) {
if (!req.get("Authorization")) {
return res.status(401).json({ error: "Требуется авторизация" });
}
next();
}
app.get("/profile", requireAuth, getProfile);Порядок выполнения
Express обходит middleware и маршруты сверху вниз:
app.use(firstMiddleware);
app.use(express.json());
app.use("/api", apiRouter);
app.use(notFound);
app.use(errorHandler);Если middleware не отправило ответ и не вызвало next(), запрос останется незавершённым.
Код после next() выполняется при обратном выходе из цепочки:
app.use((req, res, next) => {
console.log("До next");
next();
console.log("После next");
});Обычное middleware имеет три параметра. Middleware ошибок — четыре:
function errorHandler(error, req, res, next) {}Объекты req и res
req представляет входящий запрос, res — формируемый ответ.
Основные свойства req
| Свойство | Значение |
|---|---|
req.method |
HTTP-метод |
req.params |
Параметры пути |
req.query |
Query-параметры |
req.body |
Разобранное тело |
req.headers |
Заголовки |
req.path |
Путь без query-строки |
req.originalUrl |
Исходный URL |
req.ip |
IP-адрес клиента |
Получение заголовка:
const token = req.get("Authorization");Проверка формата тела:
if (!req.is("application/json")) {
return res.status(415).json({ error: "Ожидается JSON" });
}Данные в params, query, body и заголовках поступают от клиента. Им нельзя доверять без проверки.
Основные методы res
res.send("Hello");
res.json({ status: "ok" });
res.status(201).json({ id: 1 });
res.sendStatus(204);
res.redirect("/login");
res.set("Cache-Control", "no-store");Частые HTTP-статусы:
| Статус | Значение |
|---|---|
200 |
Успешный запрос |
201 |
Ресурс создан |
204 |
Успешно, без тела |
400 |
Некорректный запрос |
401 |
Требуется аутентификация |
403 |
Доступ запрещён |
404 |
Ресурс не найден |
409 |
Конфликт состояния |
422 |
Ошибка валидации |
500 |
Ошибка сервера |
После отправки ответа функцию обычно завершают через return:
if (!user) {
return res.status(404).json({ error: "Пользователь не найден" });
}
res.json(user);Иначе можно случайно отправить ответ дважды и получить ошибку Cannot set headers after they are sent.
Централизованная обработка ошибок
Пользовательский класс ошибки:
export class AppError extends Error {
constructor(statusCode, message, details) {
super(message);
this.name = "AppError";
this.statusCode = statusCode;
this.details = details;
}
}Использование:
throw new AppError(404, "Пользователь не найден");Единый обработчик:
export function errorHandler(error, req, res, next) {
if (res.headersSent) {
return next(error);
}
const statusCode = error.statusCode ?? 500;
const isServerError = statusCode >= 500;
if (isServerError) {
console.error({
message: error.message,
stack: error.stack,
method: req.method,
url: req.originalUrl
});
}
res.status(statusCode).json({
error: isServerError
? "Внутренняя ошибка сервера"
: error.message,
...(error.details && { details: error.details })
});
}Подключение выполняется в конце:
app.use("/api/users", usersRouter);
app.use(notFound);
app.use(errorHandler);Для Express 4 асинхронные контроллеры часто оборачивают:
export function asyncHandler(handler) {
return (req, res, next) => {
Promise.resolve(handler(req, res, next)).catch(next);
};
}router.get(
"/:id",
asyncHandler(async (req, res) => {
const user = await usersService.getById(req.params.id);
res.json(user);
})
);В Express 5 отклонённые Promise из async-обработчиков автоматически передаются обработчику ошибок.
В production не следует возвращать клиенту стек, пути файловой системы, запросы к базе, ключи и внутренние сообщения сервисов.
Валидация входных данных
Проверять нужно req.params, req.query, req.body, важные заголовки и файлы.
Ручное middleware:
function validateCreateUser(req, res, next) {
const { name, email, age } = req.body;
const errors = [];
if (typeof name !== "string" || name.trim().length < 2) {
errors.push({ field: "name", message: "Некорректное имя" });
}
if (typeof email !== "string" || !email.includes("@")) {
errors.push({ field: "email", message: "Некорректный email" });
}
if (age !== undefined && (!Number.isInteger(age) || age < 0)) {
errors.push({ field: "age", message: "Некорректный возраст" });
}
if (errors.length) {
return res.status(422).json({
error: "Ошибка валидации",
details: errors
});
}
req.body = {
name: name.trim(),
email: email.trim().toLowerCase(),
...(age !== undefined && { age })
};
next();
}
router.post("/", validateCreateUser, createUser);Нужно различать отсутствие поля и ложное значение:
if (req.body.count === undefined) {
// Поле отсутствует
}Проверка if (!req.body.count) также отклонит допустимый 0.
Для сложных структур используют схемы Zod, Joi или express-validator. Общий принцип:
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: "Ошибка валидации",
details: result.error.issues
});
}
req.body = result.data;
next();Различия терминов:
- валидация проверяет допустимость;
- нормализация приводит значение к единому формату;
- санитизация нейтрализует потенциально опасные данные.
Не передавайте req.body напрямую в модель базы данных. Явно выбирайте разрешённые поля:
const { name, email } = req.body;
const user = await repository.create({ name, email });Работа со статикой
Структура проекта:
project/
├── public/
│ ├── index.html
│ ├── styles.css
│ └── images/logo.svg
└── src/app.jsПодключение:
app.use(express.static("public"));Файл public/styles.css будет доступен как /styles.css.
Надёжнее использовать абсолютный путь:
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const publicDirectory = path.join(__dirname, "../public");
app.use(express.static(publicDirectory));URL-префикс:
app.use("/static", express.static(publicDirectory));Теперь файл доступен как /static/styles.css.
Настройки кэширования:
app.use(
"/static",
express.static(publicDirectory, {
maxAge: "1d",
etag: true,
index: false
})
);Долгое кэширование лучше использовать для файлов с хешем в имени, например app.a4f81c.js.
Структурирование приложения
По мере роста приложения HTTP-слой и бизнес-логику разделяют:
src/
├── app.js
├── server.js
├── routes/
│ └── users.routes.js
├── controllers/
│ └── users.controller.js
├── services/
│ └── users.service.js
├── repositories/
│ └── users.repository.js
├── middleware/
│ ├── validate-user.js
│ ├── not-found.js
│ └── error-handler.js
└── errors/
└── app-error.js| Слой | Ответственность |
|---|---|
| Роут | Метод, путь и цепочка middleware |
| Контроллер | Чтение req, вызов сервиса, ответ через res |
| Сервис | Бизнес-правила |
| Репозиторий | Доступ к данным |
| Middleware | Сквозная обработка запросов |
Роут:
router.get("/", getUsers);
router.get("/:id", getUserById);
router.post("/", validateCreateUser, createUser);Контроллер:
export async function createUser(req, res) {
const user = await usersService.create(req.body);
res.status(201).json({ data: user });
}Сервис:
export async function create(input) {
const existing = await usersRepository.findByEmail(input.email);
if (existing) {
throw new AppError(409, "Email уже используется");
}
return usersRepository.create(input);
}Сервису не нужны req и res. Благодаря этому бизнес-логику проще тестировать и переиспользовать.
Полезно отделять приложение от запуска сервера.
app.js:
export const app = express();
app.use(express.json());
app.use("/api/users", usersRouter);
app.use(notFound);
app.use(errorHandler);server.js:
import { app } from "./app.js";
const PORT = Number(process.env.PORT ?? 3000);
app.listen(PORT, () => console.log(`Port: ${PORT}`));Тесты смогут импортировать app, не открывая сетевой порт.
CORS в Express
CORS определяет, разрешено ли браузерному JavaScript одной origin читать ответ другой origin. Origin включает протокол, домен и порт.
Установка:
npm install corsПодключение:
import cors from "cors";
app.use(cors({
origin: "https://app.example.com"
}));Несколько разрешённых источников:
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com"
]);
app.use(cors({
origin(origin, callback) {
if (!origin || allowedOrigins.has(origin)) {
return callback(null, true);
}
callback(new Error("Origin is not allowed"));
}
}));Cookie и другие credentials:
app.use(cors({
origin: "https://app.example.com",
credentials: true,
methods: ["GET", "POST", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
exposedHeaders: ["X-Request-Id"],
maxAge: 600
}));Клиент:
fetch("https://api.example.com/profile", {
credentials: "include"
});При credentials: true нельзя возвращать Access-Control-Allow-Origin: *: нужен конкретный origin.
Для некоторых запросов браузер сначала отправляет OPTIONS — preflight. При глобальном подключении middleware cors обычно обрабатывает его автоматически.
CORS действует в браузере и не заменяет аутентификацию, авторизацию, проверку данных, защиту cookie и ограничение частоты запросов.
Общий пример
import crypto from "node:crypto";
import express from "express";
import cors from "cors";
const app = express();
const users = [];
app.disable("x-powered-by");
app.use(cors({ origin: process.env.CLIENT_ORIGIN ?? "http://localhost:5173" }));
app.use(express.json({ limit: "100kb" }));
app.use((req, res, next) => {
req.requestId = crypto.randomUUID();
res.set("X-Request-Id", req.requestId);
next();
});
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
app.get("/api/users", (req, res) => {
res.json({ data: users });
});
app.post("/api/users", (req, res, next) => {
try {
const { name, email } = req.body;
if (typeof name !== "string" || typeof email !== "string") {
throw new AppError(422, "Некорректные данные");
}
const user = {
id: users.length + 1,
name: name.trim(),
email: email.trim().toLowerCase()
};
users.push(user);
res.status(201).json({ data: user });
} catch (error) {
next(error);
}
});
app.use((req, res, next) => {
next(new AppError(404, "Маршрут не найден"));
});
app.use((error, req, res, next) => {
if (res.headersSent) return next(error);
const statusCode = error.statusCode ?? 500;
res.status(statusCode).json({
error: statusCode >= 500 ? "Ошибка сервера" : error.message
});
});
class AppError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
}
}
app.listen(process.env.PORT ?? 3000);Типичный порядок middleware
app.disable("x-powered-by");
app.use(cors(corsOptions));
app.use(express.json({ limit: "100kb" }));
app.use(express.urlencoded({ extended: true }));
app.use(requestId);
app.use(requestLogger);
app.use("/static", express.static(publicDirectory));
app.use("/api/users", usersRouter);
app.use(notFound);
app.use(errorHandler);Маршруты должны находиться до notFound, а обработчик ошибок — после обычных middleware и маршрутов.
Частые ошибки
Парсер JSON подключён после маршрута
// Неправильно
app.post("/users", createUser);
app.use(express.json());// Правильно
app.use(express.json());
app.post("/users", createUser);Middleware не продолжает и не завершает запрос
function brokenMiddleware(req, res, next) {
console.log(req.method);
// Нет next() и нет ответа
}Ответ отправляется дважды
После раннего ответа используйте return.
Error middleware имеет три параметра
Express распознаёт обработчик ошибок по четырём параметрам:
function errorHandler(error, req, res, next) {}Прямое сохранение req.body
Клиент может передать лишние свойства. Валидируйте данные и создавайте новый объект только из разрешённых полей.
Слишком широкий CORS
Не используйте открытый origin без необходимости. При credentials указывайте конкретные разрешённые origin.
Бизнес-логика находится в роуте
Роуты должны описывать HTTP-маршрутизацию, контроллеры — HTTP-взаимодействие, сервисы — бизнес-правила.
Краткие рекомендации
- Регистрируйте middleware в понятном порядке.
- Группируйте маршруты через
express.Router(). - Оставляйте контроллеры короткими.
- Выносите бизнес-правила в сервисы.
- Валидируйте
params,query,bodyи заголовки. - Не передавайте пользовательский объект напрямую в базу данных.
- Централизуйте обработку ошибок.
- Не раскрывайте внутренние детали ошибок в production.
- Ограничивайте размер тела запроса.
- Настраивайте CORS только для необходимых origin.
- Отделяйте создание
appотlisten()для удобства тестирования. - Используйте абсолютные пути при раздаче статических файлов.