Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Express.js проти Fastify проти Koa — що обрати?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Express.js, Fastify і Koa** - три різні підходи до HTTP у Node.js. Express: ~18K req/s, 500K+ пакетів, callback-ланцюги. Fastify: ~75K req/s, компілює схеми і маршрути при старті. Koa: чистий async/await, мінімальне ядро. **Правило:** навантажений API - Fastify. Знайома екосистема - Express.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Express.js, Fastify та Koa** - три Node.js фреймворки для HTTP-роутингу, які використовують принципово різні підходи до middleware, продуктивності та плагінів. ## Теорія ### TL;DR - Express: Swiss Army knife з 500K+ npm-пакетами, callback-ланцюги, працює всюди, але впирається в ~18K req/s - Fastify компілює JSON-схеми у скінченні автомати, маршрути у radix-дерево (radix trie), і видає 75K req/s в autocannon-бенчмарках - Koa (написана тією ж командою, що й Express) замінила callbacks на async/await, але має значно меншу екосистему - Правило вибору: потрібно >10K req/s або чітка валідація? Fastify. Команда вже на Express? Лишайся. Переписуєш старий Express на чистий async? Koa. ### Швидкий приклад Один і той самий POST-маршрут, що повертає тіло запиту, на трьох фреймворках: **Express (ланцюг callbacks):** ```js const express = require('express'); const app = express(); app.use(express.json()); app.post('/echo', (req, res) => { res.json(req.body); // Без валідації - пропускає будь-який JSON }); app.listen(3000); ``` **Koa (async-контекст):** ```js const Koa = require('koa'); const app = new Koa(); app.use(async (ctx) => { if (ctx.path === '/echo') { ctx.body = ctx.request.body; // Потрібен koa-body } }); app.listen(3000); ``` **Fastify (валідація через схему):** ```js const fastify = require('fastify')({ logger: true }); fastify.post('/echo', { schema: { body: { type: 'object', properties: { msg: { type: 'string' } } } } }, (req, reply) => { reply.send(req.body); // Валідовано до запуску хендлера }); fastify.listen({ port: 3000 }); ``` Fastify відхиляє некоректні дані ще до того, як хендлер запускається. Express мовчки їх пропускає. ### Головна різниця Express будує шаровий роутер з RegExp-матчингом по всіх зареєстрованих шляхах на кожен запит - O(n*m). При 50+ маршрутах це стає відчутним. Fastify при старті компілює маршрути у radix-дерево (O(k), де k - довжина шляху) і JSON-схеми у функції серіалізації через fast-json-stringify, обходячи динамічний доступ до властивостей у V8. До моменту коли запит доходить до хендлера, і валідація, і серіалізація вже є скомпільованим кодом. Koa навмисно нічого з цього не робить. Ти отримуєш об'єкт контексту (`ctx`) і чистий middleware-стек через `await next()`, але без автоматичної оптимізації. Autocannon-бенчмарки на маршруті "hello world" показують: Fastify 75K req/s, Koa 25K, Express 18K. ### Коли що обирати - Навантажений API (10K+ req/s): Fastify. Схема блокує погані дані і прискорює серіалізацію одночасно. - Швидкий прототип або команда вже на Express: Express. Екосистема npm не має рівних - 500K+ пакетів, passport, morgan, все є. - TypeScript-first мікросервіс: Fastify. Нативні generics (`FastifyInstance`), не потрібен `@types/`. - Переписуєш Express-callbacks на чистий async: Koa. Модель delegation (передача контролю через `await next()`) робить middleware-композицію читабельною. - Легасі-кодова база: Express. Нульова переробка існуючих маршрутів і middleware. ### Таблиця порівняння | Параметр | Express.js (v4.19) | Fastify (v5.0) | Koa (v2.15) | |---------|---------------------|----------------|-------------| | Перший реліз | 2010 | 2016 | 2014 | | Стиль middleware | Ланцюг callbacks (`app.use(fn)`) | Async-хуки + плагіни (`fastify.register()`) | Async/await (`await next()`) | | Продуктивність (autocannon) | ~18K req/s | ~75K req/s | ~25K req/s | | Парсинг JSON | Ручний (`express.json()`) | На основі схем (fast-json-stringify) | Ручний (`koa-body`) | | TypeScript | `@types/express` | Нативні generics | `@types/koa` | | Розмір | ~50KB | ~200KB з плагінами | ~20KB | | Екосистема | 500K+ npm-пакетів | 1K+ офіційних плагінів | ~10K пакетів | | Коли обирати | Моноліти, MVP | Мікросервіси, навантажені API | Чисті async-переробки | ### Як компілюються маршрути і схеми Node.js `http.Server` генерує подію `request` для кожного з'єднання. Express перехоплює її і послідовно проходить через всі зареєстровані шари, виконуючи RegExp-матчинг. Fastify вставляє radix-дерево між сервером і хендлерами. Маршрут `/api/users/:id` компілюється у вузол дерева при старті, тому матчинг під час запиту - це один прохід O(k). JSON-схема через Ajv компілюється у функцію валідації теж при старті, а не при кожному запиті. fast-json-stringify робить те саме для серіалізації. Запит доходить до хендлера, а вся підготовча робота вже виконана. Koa цього не робить. Мінімалізм - це її свідомий вибір. ### Типові помилки **Ігнорування схем у Fastify:** ```js // Неправильно - втрачаєш валідацію і ~50% приросту швидкості fastify.post('/users', (req, reply) => { reply.send(req.body); }); // Правильно - компілюється у скінченний автомат, валідує до хендлера fastify.post('/users', { schema: { body: { type: 'object', properties: { name: { type: 'string' } } } } }, (req, reply) => { reply.send(req.body); }); ``` **Вбудована auth-логіка прямо в маршрут Express:** ```js // Неправильно - callbacks вкладаються, помилки важко зловити централізовано app.get('/users', (req, res, next) => { if (!req.user) return next(); res.json(users); }); // Правильно - один middleware, всі маршрути захищені app.use(authMiddleware); app.get('/users', handler); ``` **Неправильний порядок middleware в Koa:** ```js // Неправильно - заголовок встановлюється після відповіді, губиться app.use(async (ctx) => { ctx.body = 'response'; // хендлер перший }); app.use(async (ctx, next) => { ctx.set('X-Foo', 'bar'); // запізно - body вже задане await next(); }); // Правильно - заголовки і логування - назовні app.use(async (ctx, next) => { ctx.set('X-Foo', 'bar'); await next(); // потім хендлер }); app.use(async (ctx) => { ctx.body = 'response'; }); ``` **Відсутність `ctx.throw()` для помилок у Koa:** ```js // Неправильно - за замовчуванням 404, виглядає як відсутній маршрут у логах app.use(async (ctx) => { if (!valid) ctx.body = 'error'; // статус залишається 404 }); // Правильно - встановлює коректний HTTP-статус app.use(async (ctx) => { if (!valid) ctx.throw(400, 'Invalid input'); }); ``` ### Де використовується - Express: API-шлюзи Netflix (middleware для rate-limiting), інтеграції з Mongoose ODM, будь-що де "просто встанови пакет" - Fastify: PaaS-рантайм Platform.sh, GraphQL через Mercurius (Fastify-нативний GraphQL-сервер для навантажень рівня Spotify), TypeScript-мікросервіси з чіткими схемними контрактами - Koa: кастомні сервери невеликих команд, що переписують старий Express-код; async-важкі застосунки на кшталт бекендів WeChat mini-programs Бачив команди, що витрачали тиждень на міграцію навантаженого внутрішнього API з Express на Fastify і знижували p99-затримку на 40%. Більшість приросту давала схемно-компільована серіалізація замість ручного `JSON.parse` у кожному хендлері. ### Питання на співбесіді **Q:** Чому Fastify швидший за Express на рівні роутингу? **A:** Матчинг маршрутів через radix-дерево (O(k) на запит) проти RegExp-матчингу Express по всіх зареєстрованих маршрутах (O(n*m)). Серіалізація JSON компілюється у функцію при старті через fast-json-stringify, замість динамічного `JSON.stringify` на кожен запит. **Q:** Що зламається при міграції з Express на Fastify? **A:** `res.json()` стає `reply.send()`. `app.param()` у Fastify не існує - замість нього хуки. Middleware через `app.use()` потребує `fastify.register()` або прошарку сумісності `@fastify/express`. Не всі Express-middleware працюють напряму. **Q:** Як відрізняється обробка помилок у трьох фреймворках? **A:** Express: `next(err)` передає до error-handling middleware з 4 параметрами. Koa: `ctx.throw()` або `try/catch` у найзовнішньому middleware. Fastify: `setErrorHandler()` реєструє типізований обробник, помилки логуються через Pino у структурованому форматі за замовчуванням. **Q:** Як правильно бенчмаркувати перед вибором? **A:** Запусти `autocannon -c 100 -d 20 -p 10 localhost:3000` на представницькому маршруті з реальними DB-викликами, а не просто "hello world". На схемно-валідованих маршрутах Fastify дає 70K+ rps, Express близько 15K. Розрив звужується коли справжній bottleneck - I/O, а не CPU. **Q:** Які підводні камені з TypeScript у кожному фреймворку? **A:** Fastify має нативні generics: `FastifyRequest<{ Body: CreateUserBody }>` дає повну типову безпеку без додаткових налаштувань. Express потребує `RequestHandler<Params, Body, Query>` з `@types/express`. У Koa типізація контексту найслабша - `ctx.request.body` за замовчуванням має тип `unknown`. ## Приклади ### Той самий ендпоінт створення користувача на трьох фреймворках **Fastify з preHandler-хуком:** ```js const fastify = require('fastify')(); fastify.register(async (f) => { // Симульована авторизація - заміни на реальний JWT-decode f.addHook('preHandler', async (req) => { req.user = { id: 1 }; }); f.post('/users', { schema: { body: { type: 'object', required: ['name'], properties: { name: { type: 'string' } } } } }, async (req) => { return { id: 1, name: req.body.name, owner: req.user.id }; }); }); fastify.listen({ port: 3000 }); // POST /users {"name":"Alice"} → 200 {"id":1,"name":"Alice","owner":1} // POST /users {"name":123} → 400 (схема відхиляє до хендлера) ``` **Еквівалент на Express:** ```js const express = require('express'); const app = express(); app.use(express.json()); app.use((req, res, next) => { req.user = { id: 1 }; next(); }); app.post('/users', (req, res) => { // Ручна валідація - легко пропустити під дедлайном if (typeof req.body.name !== 'string') { return res.status(400).json({ error: 'name must be a string' }); } res.json({ id: 1, name: req.body.name, owner: req.user.id }); }); app.listen(3000); // Той самий результат, але валідацію легко забути написати ``` Схема Fastify запускається до хендлера. В Express про валідацію треба пам'ятати самому. ### Шлях міграції: Express-middleware у Fastify Міграція з Express на Fastify не обов'язково відбувається за один раз. Прошарок `@fastify/express` дозволяє існуючому middleware працювати далі, поки маршрути переходять на нативний Fastify по одному: ```js const fastify = require('fastify')(); // Прошарок сумісності для існуючих Express-middleware await fastify.register(require('@fastify/express')); fastify.use(require('cors')()); fastify.use(require('helmet')()); // Нативний Fastify-маршрут поруч із legacy Express-middleware fastify.get('/api/health', async () => ({ status: 'ok' })); fastify.listen({ port: 3000 }); ``` Саме так виглядають більшість реальних міграцій: middleware залишається, маршрути переїжджають поступово.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.