🤢 Перестаньте использовать 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 для типизации запросов:

И тут есть множество вариантов ее создания:

  1. Можно писать json объекты и пробрасывать их внутрь
  2. Можно делать переменные с объектами и функциями, которые возвращаются Record<string, any>
  3. Можно использовать динамическую генерацию через Fluent Schema
  4. А можно даже писать TS типы и генерить схему из них (typescript-json-schema)

Я решил пойти по второму пути, потому что:

  1. Я хочу иметь все возможности JSON Schema и чем ближе я к JSON, тем лучше (а JS-ный Record очень к нему близок)
  2. Мне нужно иметь возможность составлять схему динамически, в зависимости от параметров (это можно сделать во 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, присоединяйтесь к моему паблику:

Всем мощной прокачки 💪

--

--