Fastify: основы и роутинг
В уроке 00 мы написали сервер на голом node:http и посчитали, сколько
инфраструктуры пришлось сделать руками: роутинг, парсинг тела,
сериализацию, обработку ошибок. Сейчас возьмём тот же сервис и поднимем его
на Fastify. Заодно ответим на главный вопрос серии.
Что такое Fastify? Неправильный, но популярный ответ: “это Express, только быстрее”. Правильный: Fastify это эффективная система плагинов над HTTP-сервером. Скорость, приятный побочный эффект (быстрый роутер, быстрая сериализация по схеме), но не суть. Суть в том, как Fastify даёт собирать приложение из изолированных кусков. Эту мысль раскроем в уроках 02-03; в этом уроке заложим фундамент: базовый сервер, роутинг, ответ, логи, ошибки.
1. Базовый сервер
import Fastify from 'fastify';
const app = Fastify({ logger: true });
app.get('/health', async () => {
return { status: 'ok' };
});
const start = async () => {
try {
await app.listen({ port: 3000 });
} catch (error) {
app.log.error(error);
process.exit(1);
}
};
void start();
Что здесь поменялось по сравнению с node:http:
Fastify({ logger: true })создаёт инстанс приложения и сразу включает структурное логирование (разберём в § 5).app.get('/health', handler)декларирует роут: метод, путь, обработчик. Никакогоif (req.method === 'GET' && req.url === '/health').- Обработчик просто возвращает объект. Fastify сам сериализует его в
JSON и проставит
Content-Type: application/json. Ручнойres.writeHeadиJSON.stringifyисчезли. app.listen({ port: 3000 })это промис. Ошибку старта (порт занят) ловим и логируем.
1.1 Тот же сервис из урока 00
Сравним лоб в лоб. Вот POST /echo, который в уроке 00 занял дюжину строк
с ручной сборкой тела из стрима:
app.post('/echo', async (request) => {
// тело уже собрано из стрима и распарсено по Content-Type
return request.body;
});
Три строки против дюжины. Fastify посмотрел на Content-Type: application/json,
выбрал JSON-парсер, собрал тело из стрима и положил готовый объект в
request.body. Битый JSON отвалится ещё до обработчика с корректным
400 Bad Request, отдельный try/catch не нужен. Вся ручная работа из § 3
урока 00 ушла в одну точку, во фреймворк.
2. Роутинг
Роут это связка метод плюс путь плюс обработчик. Методы это короткие
формы: app.get, app.post, app.put, app.delete, app.patch,
app.head, app.options. Есть и полная форма app.route({ method, url, handler }), к ней вернёмся, когда дойдём до схем.
Пути бывают трёх видов.
// статический: матчится буквально
app.get('/articles', handler);
// динамический: :id это параметр, попадёт в request.params.id
app.get('/articles/:id', async (request) => {
const { id } = request.params as { id: string };
return { id };
});
// wildcard: * ловит весь остаток пути в request.params['*']
app.get('/files/*', async (request) => {
const path = (request.params as Record<string, string>)['*'];
return { path };
});
Когда роутов много, важен порядок приоритета при совпадении. Fastify
разрешает неоднозначность так: статический сегмент важнее параметра, а
параметр важнее wildcard. Если зарегистрированы /users/me,
/users/:id и /users/*, то запрос /users/me уйдёт в статический роут,
/users/42 в параметрический, а /users/a/b в wildcard. Это не зависит от
порядка регистрации, приоритет встроен в роутер.
2.1 Почему роутинг быстрый: radix-trie
Под капотом роутинга лежит библиотека
find-my-way.
Наивный роутер хранил бы список роутов и на каждый запрос перебирал бы их
по очереди регулярками: с ростом числа роутов это линейно медленнее.
find-my-way вместо этого складывает все пути в одно
radix-trie:
общие префиксы (/users/) хранятся один раз, а матчинг идёт по дереву от
корня, сегмент за сегментом. Время поиска почти не зависит от числа роутов.
Покрути дерево на виджете: добавляй роуты, вводи путь и смотри, как матчинг спускается по узлам и какой роут в итоге срабатывает.
3. Ответ: объект reply
Возврат значения из обработчика, самый частый путь, но иногда нужно
управлять ответом явно: поставить статус, заголовок, тип. Для этого есть
второй аргумент обработчика, reply.
app.post('/articles', async (request, reply) => {
const article = await createArticle(request.body);
reply.code(201).header('Location', `/articles/${article.id}`);
return article; // тело всё равно возвращаем
});
Основные методы reply (они чейнятся):
reply.code(201)ставит статус ответа (псевдонимreply.status).reply.header('Location', '...')добавляет заголовок.reply.type('text/csv')выставляетContent-Type.reply.send(payload)отправляет тело явно.
Важная развилка: return payload против reply.send(payload). В
async-обработчике оба отправляют тело, и обычно return чище: его удобнее
тестировать, он ложится на async/await. reply.send
нужен, когда тело уходит не из конца функции (например, из колбека) или
когда ты хочешь явно подчеркнуть отправку. Не смешивай в одном обработчике
return и reply.send: вернуть значение после уже отправленного ответа,
ошибка.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.