Effect HTTP и выбор фреймворка
Весь раздел мы держали в голове два полюса: Fastify как систему плагинов и Nest
как IoC-контейнер. Это финал, и он добавляет третий полюс и закрывает главный
вопрос серии: как выбирать. Третья парадигма, Effect HTTP из effect/http-api,
смотрит на сервер иначе обоих. Не «обработчик с побочными эффектами», а описание
API, которое рантайм интерпретирует.
Здесь пригодится знать, как устроен сам пакет. Раньше платформа жила в отдельном
@effect/platform со своей версией, теперь она въехала внутрь effect: одна
версия на всю экосистему. HTTP-сервер, схемы API и клиент импортируются из
effect/http и effect/http-api.
Глубоко Effect-подход разобран в разделе 25 (проект Pulse), здесь даём его как парадигму в сравнении и строим мост туда. Цель урока, не выучить третий фреймворк, а увидеть три модели рядом и научиться выбирать под задачу.
1. HTTP-сервер как описание, а не как процедура
В Fastify и Nest обработчик, это функция с побочными эффектами: читает запрос, дёргает сервисы, шлёт ответ, бросает исключение при ошибке. В Effect сервер описывается декларативно. Сначала ты объявляешь форму API на схемах, потом отдельно поставляешь реализацию.
import { Schema } from 'effect';
import { HttpApi, HttpApiGroup, HttpApiEndpoint, HttpApiSchema } from 'effect/http-api';
class ArticleNotFound extends Schema.TaggedError<ArticleNotFound>()('ArticleNotFound', {}) {}
const api = HttpApi.make('app').add(
HttpApiGroup.make('articles').add(
HttpApiEndpoint.get('byId', '/articles/:id', {
params: Schema.Struct({ id: Schema.String }),
success: Article,
error: ArticleNotFound.pipe(HttpApiSchema.status(404)),
}),
),
);
Это ещё не сервер, это контракт:
какие есть эндпоинты, какие у них пути, схемы входа, схемы успеха и какие ошибки
возможны. Схемы написаны на Schema,
поэтому одна декларация даёт сразу контракт, рантайм-валидацию и TS-типы.
2. Реализация как Layer
Обработчики подключаются отдельно, через HttpApiBuilder. Каждая группа, это
Layer,
который зависит от нужных сервисов и поставляет реализацию эндпоинтов.
import { Effect, Layer } from 'effect';
import { HttpApiBuilder } from 'effect/http-api';
const articlesLive = HttpApiBuilder.group(api, 'articles', (handlers) =>
handlers.handle('byId', ({ params }) =>
Effect.gen(function* () {
const service = yield* ArticlesService; // зависимость видна в типе Effect
return yield* service.byId(params.id); // ошибка ArticleNotFound в канале результата
}),
),
);
const apiLive = HttpApiBuilder.layer(api).pipe(Layer.provide(articlesLive));
Запуск, тоже Layer: apiLive поднимается через
NodeHttpServer из @effect/platform-node. Под капотом есть и низкоуровневый слой
(HttpRouter, HttpServerRequest, HttpServerResponse), это контраст: ближе к
Fastify по стилю, но без декларативного контракта. Высокоуровневый HttpApi,
основной способ.
Главное, что здесь видно: зависимость ArticlesService не приходит из
контейнера через декоратор, она запрашивается через yield* и видна прямо в типе
Effect. Layer-граф поставляет её композицией. Это DI без контейнера и без
рефлексии, та самая цена которой мы считали в уроке 09.
3. Что даёт декларативность бесплатно
Раз API, это значение со схемами, из него можно вывести многое, чего в Fastify и Nest пишешь руками или генерируешь отдельным шагом:
- OpenAPI и Swagger выводятся из той же декларации (
HttpApiSwagger), без отдельных декораторов@ApiProperty(Nest) и без сборки схемы из JSON Schema роутов (Fastify). - Типобезопасный клиент деривится из того же
api(HttpApiClient): фронт получает вызовы с теми же типами, без кодогена. - derive вместо синхронизации: схема, типы, валидация, документация и клиент, это проекции одной декларации. Нет ручного держания их в синхроне, главного источника рассинхрона в Fastify и Nest.
Это та же мысль, что в type providers Fastify (одна схема, типы и валидация), но доведённая до конца: из одной декларации выводится вся обвязка.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.