Express против Fastify в 2026 году: практическое сравнение фреймворков Node.js
В этом руководстве сравниваются Express и Fastify с точки зрения производительности, валидации, экосистемы и обработки ошибок; также рассматриваются основные изменения в Express 5, приводящие к несовместимости.
Представьте команду, которая начинает разработку нового бэкенда на Node.js.
Они приступают к изучению фреймворков, и почти сразу в разговоре выделяются два названия:
Express.js и Fastify.
Затем появляются графики тестов производительности.
В ходе множества синтетических тестов Fastify показывает значительно более высокие показатели пропускной способности.
Естественная реакция заключается в следующем:
"Если Fastify превосходит по скорости, зачем кому-то продолжать использовать Express?"
На первый взгляд это разумный вопрос.
Но выбор бэкенд-фреймворка редко сводится к одному лишь такому показателю.
Количество сырых запросов в секунду — это лишь один из аспектов проблемы. Необходимо также учитывать окружающую экосистему, способ работы мидлвэра, встроенную проверку данных, поддержку TypeScript, стоимость миграции, повседневный опыт работы разработчиков, существующую базу кода, которой вы управляете, а также конкретные требования вашего приложения.
Существует ещё один аспект этого сравнения, на который стоит обратить внимание.
Express 5 теперь официально выпущен.
После долгого использования Express 4 эта новая основная версия вносит ряд изменений, о которых должны знать все, кто поддерживает старую базу кода Express.
Учитывая это, давайте рассмотрим, что на самом деле отличает Express от Fastify — и, что ещё важнее, в каких ситуациях каждый из них более подходит.
Во-первых: что же такое Express и Fastify?
Оба фреймворка созданы для помощи в разработке веб-серверов на основе Node.js.
По сути, они избавляют вас от необходимости напрямую работать с низкоуровневым модулем HTTP Node, предоставляя более удобный API для определения маршрутов и обработки запросов.
Вот как выглядит минимальный сервер Express:
const express = require("express");
const app = express();
app.get("/users", (req, res) => {
res.json([
{ id: 1, name: "Neha" },
{ id: 2, name: "Rahul" }
]);
});
app.listen(3000);
Начальная точка для Fastify выглядит довольно похоже:
const fastify = require("fastify")({
logger: true
});
fastify.get("/users", async (request, reply) => {
return [
{ id: 1, name: "Neha" },
{ id: 2, name: "Rahul" }
];
});
fastify.listen({ port: 3000 });
Заметили что-нибудь?
Ни один из примеров не кажется особенно сложным.
Настоящие различия между ними становятся очевидными только тогда, когда приложение вырастает за рамки этого упрощенного примера.
Express против Fastify: общая картина
Давайте рассмотрим, почему различия между этими двумя фреймворками действительно имеют значение на практике.
1. Производительность: преимущество Fastify
Вероятно, это главная причина, по которой Fastify так часто упоминается в подобных обсуждениях.
Производительность и минимальные затраты ресурсов с самого начала были ключевыми целями проектирования Fastify.
Его внутренняя структура в значительной степени опирается на схемы как для проверки входящих данных, так и для сериализации выходных ответов, что может существенно повысить пропускную способность при работе с большим объемом запросов к API. Сама документация Fastify рекомендует использовать JSON Schema именно для проверки маршрутов и сериализации ответов.
Тем не менее здесь существует важное ограничение.
Не стоит на основе одного результата тестирования сразу делать вывод, что:
Fastify автоматически означает приложение в 3 раза быстрее.
Большинство тестов производительности фреймворков измеряют затраты самого фреймворка в строго контролируемых условиях.
Сами разработчики Fastify признают, что опубликованный ими тест производительности представляет собой синтетический тест в стиле «hello world», и они прямо рекомендуют проводить тестирование собственного реального приложения, если для вас важна производительность.
Рассмотрим поток запросов, выглядящий примерно так:
Request
↓
Authentication
↓
Database query
↓
Redis
↓
External API
↓
Business logic
↓
Response
Если один только вызов базы данных занимает 80 миллисекунд, уменьшение незначительной части нагрузки фреймворка не приведёт к значительному ускорению работы данного эндпоинта.
Поэтому настоящий вопрос, который следует себе задать, звучит так:
Является ли моё приложение ограниченным из-за нагрузки на CPU или фреймворк?
Другими словами: является ли сам фреймворк причиной замедления обработки запросов? Если ответ «да», то переход на Fastify имеет гораздо более веские основания.
Если ответ «нет», то общие тесты производительности фреймворков, скорее всего, не должны становиться определяющим фактором для принятия решения.
2. Проверка корректности: здесь Fastify становится интересным
Представьте, что ваш API разработан для принятия данных в таком формате:
{
"name": "Neha",
"age": 25
}
Естественно, вы захотите отклонить некорректную версию этих же данных, например:
{
"name": 123,
"age": "hello"
}
В экосистеме Express команды обычно используют дополнительные библиотеки для обработки подобной проверки запросов. Fastify придерживается иного подхода: проверка на основе схемы встроена непосредственно в сам фреймворк.
Вот как это выглядит на практике:
const schema = {
body: {
type: "object",
required: ["name", "age"],
properties: {
name: { type: "string" },
age: { type: "integer" }
}
}
};
fastify.post("/users", { schema }, async (request, reply) => {
return { message: "User created" };
});
Fastify позволяет определять схемы JSON для различных частей цикла запроса и ответа, включая:
- тело запроса
- параметры запроса
- параметры маршрута
- заголовки
- сериализацию ответа
По сути, Fastify использует Ajv для реализации этого слоя валидации, причем те же самые определения схем могут также использоваться для ускорения сериализации ответов.
Ajv, сокращение от «Another JSON Schema Validator», — это была библиотека, соответствующая стандартам и предназначенная для проверки объектов данных JavaScript на соответствие определениям JSON Schema; она работает как в Node.js, так и в браузере.
Такой подход, основанный на схемах, становится особенно ценным, когда API начинает обрабатывать большое количество структурированных данных входа и выхода.
3. У Express есть то, чего Fastify трудно заменить: его экосистема
Express существует с 2010 года, что означает наличие огромного объема накопленных знаний, инструментов и опыта сообщества, связанного с ним.
- Нужна аутентификация? Существует соответствующий пакет.
- Нужна логгирование? Тоже есть соответствующий пакет.
Такая глубина экосистемы важнее, чем может показаться на первый взгляд.
Представьте, что вы присоединяетесь к компании, чей бэкенд работает в производстве уже шесть лет. Вы не можете выбрать фреймворк с нуля — вам приходится использовать то, что уже есть, и оно может выглядеть примерно так:
Express
├── 200+ routes
├── authentication middleware
├── custom middleware
├── logging
├── validation
├── monitoring
└── lots of business logic
Действительно ли имеет смысл переписывать всё это только потому, что Fastify работает быстрее по результатам тестов? Вряд ли.
Миграция существующей базы кода всегда сопряжена с затратами, и любое реальное инженерное решение должно учитывать эти затраты по сравнению с ожидаемой пользой.
4. Мидлвэры против плагинов
Между этими двумя фреймворками существует ещё более глубокое архитектурное различие.
Express построен на концепции мидлвэра. Типичная конфигурация может выглядеть следующим образом:
app.use(authMiddleware);
app.use(loggingMiddleware);
app.use(express.json());
Каждый входящий запрос последовательно проходит через эти функции мидлвэра.
Fastify, в отличие от него, использует архитектуру на основе плагинов с применением хуков и инкапсуляции. Концептуально поток выполнения выглядит примерно так:
Request
↓
Fastify
↓
Hooks
↓
Plugins
↓
Route
↓
Response
Для приложений, спроектированных с самого начала с учётом модели Fastify, такая структура позволяет легче организовывать и понимать более крупные кодовые базы.
Однако здесь есть компромисс. Если вы годами формировали свои представления о мидлвэре в стиле Express, адаптация к системе плагинов и хуков Fastify может сначала показаться незнакомой.
5. Обработка ошибок
В Express 4 для обработки ошибок, возникающих у асинхронных обработчиков маршрутов, обычно требовалось их вручную перенаправлять. Типичный подход выглядел так:
app.get("/user/:id", async (req, res, next) => {
try {
const user = await getUserById(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
});
Express 5 значительно упрощает этот процесс. Теперь отклонения обещаний, возникающие у обработчиков маршрутов или промежуточного программного обеспечения, автоматически передаются в ваш промежуточный модуль обработки ошибок, что позволяет писать гораздо более лаконичный код:
app.get("/user/:id", async (req, res) => {
const user = await getUserById(req.params.id);
res.json(user);
});
Если функция getUserById() выбрасывает исключение, Express 5 автоматически его ловит и направляет в ваш обработчик ошибок, без необходимости явного использования конструкций try/catch или вызова next(err). Само по себе это кажется незначительным синтаксическим удобством. Однако в кодбазе с сотнями маршрутов такие небольшие улучшения в совокупности приводят к гораздо более аккуратному и менее подверженному ошибкам коду.
Express 5: Что на самом деле изменилось?
Express 5 появился в октябре 2024 года, поэтому к 2026 году это уже не новая версия. Тем не менее большое количество производственных приложений работает на Express 4, что делает понимание изменений между версиями крайне важным для тех, кто планирует обновление.
И да, существуют изменения, способные нарушить работу приложений, о которых необходимо знать.
1. Изменены опциональные параметры маршрутов
В Express 4 вы могли писать маршруты вот так:
app.get("/:file.:ext?", handler);
Express 5 заменяет эту схему на:
app.get("/:file{.:ext}", handler);
Старая обозначение ? для указания опциональности параметра больше не работает.
Почему произошла замена?
Новая синтаксис позволяет с первого взгляда яснее видеть, какая часть пути является опциональной.
На бумаге это незначительное изменение, но оно может тихо нарушить работу десятков маршрутов в крупном, уже существующем приложении.
2. Изменены маршруты с вайлдкартами
Ранее универсальный маршрут мог выглядеть так:
app.get("/*", handler);
Express 5 требует, чтобы волчьи знаки имели имена:
app.get("/*splat", handler);
Если вам нужно, чтобы этот волчий знак также совпадал с корневым путем /, оберните его так:
app.get("/{*splat}", handler);
Это ещё одно незначительное изменение синтаксиса, которое может тихо нарушить существующую логику маршрутизации во время обновления.
3. Изменены шаблоны маршрутов с использованием регулярных выражений
Express 5 устраняет поддержку нескольких старых шаблонов, основанных на строках и использующих символы в стиле регулярных выражений.
Например, вместо прямого включения альтернативных сегментов в одну строку пути теперь обычно передаётся массив путей:
app.get(
["/discussion/:slug", "/page/:slug"],
handler
);
Цель этого изменения — сделать совпадение маршрутов более явным и предсказуемым, вместо использования упрощений в стиле регулярных выражений.
4. Теперь req.body может быть равен undefined
Это тонкий момент, который легко упустить из виду.
В Express 4 было принято считать, что:
req.body
объект автоматически будет равен пустому объекту ещё до начала обработки данных.
В Express 5, если тело запроса не было обработано, req.body может просто быть равен undefined.
Это означает, что в зависимости от маршрута становится необходимой защитная проверка вроде этой:
if (!req.body) {
return res.status(400).send("Request body required");
}
Это незначительная изменение в поведении, но именно такие мелочи могут привести к сложным в отслеживании ошибках после обновления.
5. Изменения в функции express.urlencoded()
Теперь значение по умолчанию для опции extended — false.
Поэтому вместо того, чтобы полагаться на скрытое значение по умолчанию:
app.use(express.urlencoded());
вам следует явно установить его, если ваше приложение зависит от старого поведения:
app.use(
express.urlencoded({
extended: true
})
);
Ещё одна деталь, которую стоит тщательно проверить при миграции существующей базы кода.
6. Изменения в парсере тела запроса
Express 5 также улучшил внутреннюю работу парсера тела запроса.
Теперь вы можете писать:
app.use(express.json());
app.use(
express.urlencoded({
extended: false
})
);
вместо того чтобы, как раньше, объединять отдельные пакеты middleware body-parser.
Кроме того, Express 5 добавляет поддержку запросов с телами, сжатыми в формате Brotli, и позволяет настраивать максимальную глубину URL-кодированных данных.
7. Некоторые старые API ответов были удалены
Представьте старый приложение Express, содержащее что-то вроде:
res.send({
message: "Success"
}, 200);
В Express 5 ожидаемый формат выглядит так:
res
.status(200)
.send({
message: "Success"
});
Аналогичным образом, старый сокращённый вариант:
res.redirect("back");
был полностью удалён.
Официальный руководство по миграции советует вручную проверять заголовок referrer и использовать стандартный путь в случае его отсутствия.
Ни одна из этих отдельных настроек не является особенно сложной для реализации.
Но представьте себе кодовую базу старой версии, в которой тысячи вызовов написаны старым способом.
На таком масштабе обновление уже не является простым изменением зависимостей, а превращается в самостоятельную сложную инженерную задачу.
Итак, Express или Fastify: кто победит?
На этом этапе у вас уже достаточно информации, чтобы принять решение.
Я бы не основывал свой выбор исключительно на:
«Который работает быстрее по тестам?»
Вместо этого сначала обратите внимание на самое приложение.
Причины остаться с Express:
- Вы только начинаете заниматься разработкой бэкенда на Node.js.
- Ваша команда уже хорошо знакома с Express.
Причины рассмотреть Fastify:
- Вы создаете с нуля новый сервис, ориентированный на API.
- Пропускная способность и минимизация нагрузки на фреймворк действительно важны.
- Вы хотите, чтобы проверка данных осуществлялась непосредственно с помощью схем.
- Вы также хотите, чтобы сериализация обрабатывалась с использованием схем.
- Вы разрабатываете микросервисы.
- Вам нравится работа с архитектурой, основанной на плагинах.
- Вы готовы использовать более молодую экосистему.
В самом факте выбора Express даже для крупномасштабной системы нет ничего плохого.
То, что приложение является «крупным», не означает автоматически, что Fastify — лучший выбор.
Общая архитектура, в которой используется фреймворк, обычно имеет гораздо большее значение, чем сам выбор фреймворка.
Основной способ принятия решения
Вот приблизительная последовательность размышлений для принятия решения:
Start
│
▼
Is this an existing app?
/ \
Yes No
│ │
▼ ▼
Already using Need very high
Express? throughput?
/ \ / \
Yes No Yes No
│ │ │ │
▼ ▼ ▼ ▼
Keep Evaluate Fastify Evaluate
Express migration both
Прежде чем окончательно определиться, стоит задать ещё один вопрос:
Какую проблему я на самом деле пытаюсь решить?
Если ваше существующее приложение на Express работает медленно, не спешите сразу винить фреймворк.
Сначала проанализируйте его производительность.
Настоящей причиной может быть:
Slow API
↓
Database query
↓
Missing index
или:
Slow API
↓
External API
↓
3-second response time
или:
Slow API
↓
Expensive business logic
↓
CPU bottleneck
Смена фреймворка сама по себе не решит ни одной из этих основных проблем.
Мое мнение по этому вопросу
Если сегодня вы начинаете новый небольшой проект на Node.js, отказываться от Express только потому, что Fastify показывает лучшие результаты тестов, не имеет особого смысла. Express обладает огромной экосистемой, простой моделью понимания и многолетними накопленными знаниями сообщества.
Тем не менее Fastify тоже нельзя игнорировать. Для нового сервиса, где действительно важны пропускная способность, проверка схемы, сериализация и минимальное количество нагрузки от фреймворка, Fastify заслуживает серьезного рассмотрения.
А если у вас уже есть приложение на Express 4? Переписывать его исключительно из-за того, что Fastify быстрее, будет неправильным решением.
Лучший подход — сначала изучить приложение, определить места настоящих узких мест, проверить совместимость с Express 5, пройти тесты миграции, и только затем решить, действительно ли смена фреймворка приносит достаточную пользу, чтобы оправдать усилия.
Вероятно, в этом и заключается главный вывод.
Фреймворк с лучшими показателями не обязательно является подходящим решением для вашей ситуации.
Подходящим фреймворком будет тот, который решает вашу конкретную проблему, добавляя при этом как можно меньше ненужной сложности.
Связанные материалы
- Сравнение Node.js, Deno и Bun: тесты производительности, компромиссы и стратегия миграции — Рассматриваются реальные архитектурные различия между Node.js, Deno и Bun, что показывают тесты производительности за 2025 год, и как определить, стоит ли мигрировать и когда это сделать.
- Общее использование одной схемы Zod между фронтендом на React и бэкендом на Node — Узнайте, как одна схема Zod может проверять формы в React, ответы API, тела запросов Express и переменные окружения, одновременно генерируя соответствующие типы TypeScript.