Що таке Express.js і чому його використовують?
Express.js - це мінімальний веб-фреймворк для Node.js, який додає маршрутизацію, middleware та HTTP-утиліти поверх вбудованого модуля http.
Теорія
TL;DR
- Express для Node.js, як підготовча станція для кухаря: Node дає тобі піч і сировину, Express додає ножі, дошки і рецепти
- Головна різниця: 15+ рядків ручного парсингу запитів у Node.js стають 3 декларативними рядками у Express
- Express автоматично обробляє URL-маршрутизацію, парсинг JSON і заголовки відповідей
- Правило вибору: бери Express для будь-якого Node.js веб-додатку, якщо не потрібні нуль залежностей або 10k+ req/s (тоді Fastify)
Швидкий приклад
// Чистий Node.js: ручна маршрутизація, ручні заголовки, все вручну
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/users' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([{ id: 1, name: 'Alice' }]));
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000);
// Express: той самий результат, 3 рядки
const express = require('express');
const app = express();
app.get('/users', (req, res) => res.json([{ id: 1, name: 'Alice' }]));
app.listen(3000);Обидва дають однакову відповідь. Express прибирає весь рядковий пошук по req.url і ручне виставлення заголовків.
Що Express насправді робить
Модуль http у Node.js дає тобі сирий об'єкт запиту: рядок URL, рядок методу і тіло як буфер. Все потрібно парсити самостійно. Express обгортає цей об'єкт роутером (radix-дерево для швидкого пошуку шляхів), підключає парсери через middleware і дає тобі res.json(), res.status() та інші хелпери. Ти пишеш обробники маршрутів замість ланцюжків if/else.
Стек middleware (проміжних функцій) - це центральна ідея. Кожен виклик app.use() додає функцію в чергу. Коли приходить запит, Express запускає кожну функцію по порядку, передаючи через них req і res. Якщо middleware викликає next(), запускається наступна функція. Якщо викликає res.send(), ланцюжок там і зупиняється.
Коли використовувати Express
- REST API або веб-додаток: Express - вибір за замовчуванням
- Прототипування: найшвидший шлях від ідеї до запущеного сервера
- Full-stack додаток з авторизацією і базою даних: Express плюс стек middleware (cors, helmet, body-parser)
- Serverless або edge-функції з багатьма маршрутами: Express підходить; менше 10 маршрутів, hono.js буде легшим
- Мікросервіс з кастомним протоколом: пропускай Express, використовуй
httpнапряму - 10k+ запитів на секунду в продакшені: Fastify приблизно вдвічі швидший; Express впевнено тягне до ~5k req/s
Альтернативи на вибір
| Фреймворк | Стиль | Найкраще підходить |
|---|---|---|
| Express | Мінімальний, гнучкий | REST API, full-stack додатки |
| Fastify | Швидкий, на основі схем | Навантажені API |
| Koa | Async-first, менше ядро | Нові проекти, кастомний middleware |
| NestJS | Структурований, TypeScript | Корпоративні додатки |
| hono.js | Компактний, для edge | Serverless, Cloudflare Workers |
Koa варто знати для співбесіди. Він використовує async/await нативно там, де Express використовує callbacks, і його ядро значно менше. Для нових проектів Koa - цілком валідний вибір. Коли потрібна велика екосистема плагінів, Express виграє.
Типові помилки
Відсутній express.json() перед маршрутами. Це ламає приблизно 40% перших Express-додатків. Без middleware req.body залишається undefined, і будь-який доступ до JSON падає з помилкою.
// Неправильно: req.body тут undefined
app.post('/user', (req, res) => res.send(req.body.name)); // TypeError
// Правильно: парсити JSON глобально перед будь-яким маршрутом
app.use(express.json());
app.post('/user', (req, res) => res.send(req.body.name)); // "Alice"Неправильний порядок middleware. Middleware запускається зверху вниз. Якщо зареєструвати маршрут до express.json(), той маршрут отримає непарсинговане тіло.
// Неправильно: маршрут бачить req.body як undefined
app.post('/api/user', (req, res) => res.send(req.body.name));
app.use(express.json()); // запізно
// Правильно
app.use(express.json()); // обов'язково першим
app.post('/api/user', (req, res) => res.send(req.body.name)); // працюєПодвійний виклик res.send(). Node кидає "Cannot set headers after they are sent". Виправляється раннім return.
// Неправильно: два send в одному обробнику
if (err) res.status(500).send('Error');
res.json({ ok: true }); // краш
// Правильно
if (err) return res.status(500).send('Error');
res.json({ ok: true });Відсутній next() у middleware. Запит зависає. Клієнт не отримує відповідь, в продакшен-моніторах спрацьовують таймаути.
// Неправильно: запит зависає назавжди
app.use((req, res, next) => {
console.log(req.method);
// забули next()
});
// Правильно
app.use((req, res, next) => {
console.log(req.method);
next();
});Де зустрічається в реальних проектах
- Strapi: headless CMS з 1M+ завантаженнями, побудований на Express для REST-ендпоінтів і адмін-панелі
- Ghost: блогова платформа, Express обробляє динамічні маршрути і теми
- PayloadCMS: Express плюс TypeScript для e-commerce бекендів, 10k+ розробників
- Next.js API routes: внутрішньо використовує Express-подібний роутер для обробників
/pages/api - Більшість внутрішніх Node.js інструментів стартують з Express, а до Fastify переходять лише тоді, коли throughput стає реальною проблемою
Я помічав, що команди використовують Express роками без досягнення його performance-межі, бо справжнє вузьке місце - це запит до бази даних. Якщо запит займає 50ms, overhead Express у 0.1ms просто не має значення.
Питання на співбесіді
Q: Що таке middleware у Express і як це працює?
A: Middleware - це функція з сигнатурою (req, res, next). Express викликає їх по порядку для кожного запиту. Використовується для парсингу тіла, перевірки авторизації, логування або додавання даних до req. Якщо middleware не викликає next(), ланцюжок зупиняється і клієнт не отримує відповідь.
Q: Як Express обробляє async-помилки?
A: Автоматично - ніяк. Якщо в async-обробнику виникає виняток, Express його не зловить. Потрібно або загортати кожен обробник у try/catch і викликати next(err), або використовувати пакет express-async-errors, який патчить роутер для перехоплення відхилень Promise.
Q: В чому різниця між app.get() і router.get()?
A: app - це глобальний екземпляр Express. router - це міні-Express-додаток, який можна монтувати на будь-який шлях. Використовуй router для групування маршрутів за префіксом: app.use('/api', router) монтує всі маршрути роутера під /api. Це тримає великі кодові бази модульними без одного гігантського файлу.
Q: Як маршрутизація Express масштабується до сотень маршрутів?
A: Express використовує radix-дерево (layered path trie) для пошуку маршрутів. Пошук займає O(k), де k - довжина шляху, а не O(n) маршрутів. Тому 1000 маршрутів не уповільнять пошук порівняно з 10.
Q: Express проти Koa - коли обрати Koa?
A: Koa використовує async/await нативно і має менше ядро без вбудованого middleware. Гарний вибір для нових проектів, де потрібен повний контроль над стеком. Express має більшу екосистему плагінів і більше прикладів у спільноті. Команди на наявних кодових базах зазвичай залишаються з Express, бо вартість переходу не виправдана.
Приклади
Базовий REST API з парсингом JSON
const express = require('express');
const app = express();
app.use(express.json()); // парсинг JSON у тілі запиту
app.get('/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }]);
});
app.post('/users', (req, res) => {
const { name } = req.body; // працює завдяки express.json()
res.status(201).json({ id: 2, name });
});
app.listen(3000, () => console.log('Запущено на порту 3000'));app.use(express.json()) виконується перед кожним маршрутом. POST до /users з JSON-тілом автоматично отримує заповнений req.body. Без цього рядка req.body залишається undefined і деструктуризація падає.
Обробник webhook з правильним порядком middleware
Ось як виглядає перевірка Stripe webhook у реальному Express-додатку:
const express = require('express');
const app = express();
// Сирий буфер потрібен для перевірки підпису - обов'язково до express.json()
app.use('/webhook/stripe', express.raw({ type: 'application/json' }));
app.use(express.json()); // JSON для всього іншого
app.post('/webhook/stripe', (req, res) => {
const sig = req.headers['stripe-signature'];
if (!verifySignature(sig, req.body)) {
return res.status(400).send('Invalid signature');
}
console.log('Event:', req.body.type); // 'payment_intent.succeeded'
res.json({ received: true });
});
app.listen(3000);Маршрут /webhook/stripe потребує сирого буфера для перевірки підпису. Все інше отримує парсингований JSON. Порядок middleware контролює, який парсер спрацює першим, тому специфічний маршрут реєструється до глобального. Це реальний патерн з PayloadCMS та подібних проектів.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.