🤢 Перестаньте использовать express / koa и начните использовать Fastify
Я стараюсь избегать HTTP API на большинстве своих проектов, используя GQL для фронтовых API и gRPC для микросервисного общения (или можно, например, какую-нибудь MQ + protobuf для типизации сообщений).
При построении абсолютного любого API мне нужны:
1. Схема API с возможностью интроспекции (трансформации схемы в типизацию языка)
2. UI с демонстрацией API (например, swagger)
3. Типизация функционала библиотеки (чтобы все request и response правильно типизировались по схеме)
4. Миддлвары, logger, контекст для DI, плагины, etc.
Express и koa — библиотеки с минимальным набором функционала и добавить туда вышеописанные пункты можно при помощи сторонних библиотек. C 4-м пунктом сторонние библиотеки более менее справляются, а вот с первыми тремя постоянно возникают какие-то проблемы.
Я не хочу каждый раз заморачиваться с добавлением функционала, который нужен мне на каждом проекте.
Но есть библиотека, которая решает все эти вопросы разом— Fastify
У него встроено работа с JSON Schema, у него есть очень функциональный плагин для работы с Swagger и он замечательно дружит с TS.
В этой статей хочу поделиться техниками и библиотеками, которые я использую с Fastify.
(не хочу тратить на статью много времени, поэтому вкратце пробегусь по основным моментам, если хотите больше, пишите комментарии, я раскрою подробнее)
Настройка
Fastify требует совершенно небольшого кол-ва настроек:
В моем случае, я передаю заранее инициализированный logger (я использую Pino, потому что это один из самых зарекомендовавших себя логеров и Fastify по дефолту тоже использует именно его).
А также, меняю автогенерирующийся id запроса на uuid вместо инкремента: инкремент при старте приложения всегда начинается с 0, поэтому в логах у вас будут куча запросов с request id 0 / 1 / 2 / 3, а uuid всегда уникальный, так что они повторяться не будут.
Swagger
Для работы со swagger я использую библиотеку fastify-swagger
Чтобы она корректно заработала, нужно добавить ее как плагин к инстансу Fastify:
Тут все просто и можно действовать по документации, НО самое главное это поле “openapi”.
Дело в том, что “Swagger” поддерживает старую JSON Schema, если вы хотите продвинутые функции современной JSON Schema вам потребуется OpenApi v3.
И именно настройка поля “openapi” включит эту возможность.
Error handling
Fastify отдает ошибки достаточно красиво, но я привык использовать немного другую структуру ответов, поэтому кастомизирую возврат на ошибки.
Для этого у нас есть функция “setErrorHandler”:
Если какой либо из обработчиков запроса выбросил ошибку или она возникла в каком-либо мидлваре (например, при валидации), она попадет сюда.
Как видите TS типы уже хорошо видны + можно указать свои типы ошибок в треугольных скобочках дженерика (там где <FastifyErrorE>).
Есть еще хук “OnError”, но я не советую его использовать, потому что он вызывается не при всех lifecycle.
Также, если вы решили сделать свой кастомный ответ на ошибки, не забудьте переписать ошибку 404:
JSON Schema
А вот это интересно. Fastify из коробки поддерживает JSON Schema для типизации запросов:
И тут есть множество вариантов ее создания:
- Можно писать json объекты и пробрасывать их внутрь
- Можно делать переменные с объектами и функциями, которые возвращаются Record<string, any>
- Можно использовать динамическую генерацию через Fluent Schema
- А можно даже писать TS типы и генерить схему из них (typescript-json-schema)
Я решил пойти по второму пути, потому что:
- Я хочу иметь все возможности JSON Schema и чем ближе я к JSON, тем лучше (а JS-ный Record очень к нему близок)
- Мне нужно иметь возможность составлять схему динамически, в зависимости от параметров (это можно сделать во 2-м и 3-м случаях)
Примерно, так выглядит схема запроса:
(чтобы понять почему это выглядит именно так, нужно понимать как строиться JSON Schema)
Это типизация query запроса (все, что идет после “?” в адресе).
Единственная особенность – это “as const” в конце, но это объясню позже.
Можно также, было бы сделать это функцией:
Так, я могу динамически конфигурировать валидацию минимальной длины значения “foo” хоть при старте приложения, хоть в процессе его исполнения.
А вот так выглядит добавление этой схемы к роуту:
Эту схемы автоматически подхватит swagger и сгенерирует из нее доку.
А что насчет TS?
И тут тоже все хорошо:
Чтобы превратить схему в TS типы, мы воспользуемся библиотекой
json-schema-to-ts
Я очень боялся, что она кинет меня через плечо, но нет, она работает потрясающе (и даже поддерживает инварианты через “oneOf”, “allOf”, etc.)
Чтобы добавить типизацию к endpoint нужно сделать следующее:
Я добавил в дженерик переменную поле “Querystring”, воспользовался функцией “FromSchema” из библиотеки json-schema-to-ts, дальше “ReturnType” потому что сейчас QueryStringSchema – это функция и в конце “typeof”, потому что мне нужен именно тип ее возврата.
Можно опустить “ReturnType”, если схема у вас как переменная, а не функция.
Единственный минус, который могу отметить: при ошибке “FromSchema” абсолютно непонятно где конкретно она допущена, но можно точно знать, что это вы где-то напортачили с описанием схемы или забыли поставить “as const” (это нужно TS, чтобы правильно прочитать типы).
Middleware
Что не всегда очевидно с самого начала: в Fastify нет стандартного понятия “мидлвары”, вместо нее используются хуки и контексты.
Контекст – можно сказать, что это форк приложения со своей областью видимости и у каждого контекста есть свои хуки.
Подробнее про контекст:
Подробнее про хуки:
Подобного рода комбинации сложнее, чем в koa или express, но при этом более функциональная.
Заключение
Все это дело, работает из коробки и работает хорошо, поэтому впервые за долгое время я получил удовольствие от написания HTTP API.
Если у вас есть вопросы, пишите в комментарии, с удовольствием отвечу и раскрою темы.
Если хотите больше контентов на тему продвинутой разработке на Node.js, присоединяйтесь к моему паблику:
Всем мощной прокачки 💪