Angular и tRPC

В Twitter и Youtube мы слышали, как многие разработчики React говорили о tRPC. Нам стало любопытно, и захотели попробовать это в Angular. И как же это сделать?
tRPC - это легкий, высокопроизводительный фреймворк RPC (удаленный вызов процедуры), разработанный для того, чтобы быть простым, быстрым и удобными в использовании.
Это позволяет вам совершать вызовы серверу от клиента, как если бы сервер был локальным объектом, и позволяет создавать распределенные системы с использованием различных языков программирования.
Основным преимуществом использования tRPC является безопасность типов без генерации кода.
Настройка сервера fastify с tRPC
Чтобы настроиться на сервере tRPC, мы сначала должны запустить новый проект npm и установить необходимые пакеты.
tRPC можно комбинировать с различными серверными фреймворками узлов, такими как express или fastify. На протяжении всего этого поста в блоге мы будем использовать fastify. Кроме того, мы будем использовать zod для проверки входящих данных запроса.
npm init
npm i @trpc/server fastify fastify-cors zod
Как только мы установили пакеты. мы можем пойти дальше и сгенерировать файл server.ts
.

Хорошо, если вы когда-либо использовали fastify, этот код выглядит очень знакомым. Мы запускаем сервер fastify на порту 3000
и включаем CORS. Однако здесь есть несколько очень интересных строк, специфичных для tRPC. Давайте поговорим о них.
Во-первых, мы импортируем fastifyTRPCPlugin
из @trpc/server/adapters/fastify
. Затем мы используем этот плагин для регистрации маршрутизатора tRPC на нашем сервере fastify.
Маршрутизатор еще не существует; давайте продолжим и создадим его.
tRPC-маршрутизатор
В системе tRPC маршрутизатор направляет входящие запросы на соответствующий сервер или службу. Маршрутизатор действует как центральная точка связи, перенаправляя клиентские запросы на нужный сервер и возвращая ответы клиенту.
Маршрутизатор tRPC обычно выполняет следующие действия:
- Принимает входящие запросы от клиентов
- Сопоставляет запрос соответствующему серверу или службе на основе метода и имени службы
- Перенаправляет запрос на правильный сервер или службу
- Получает ответ от сервера или службы
- Возвращает ответ клиенту
Основное преимущество использования маршрутизатора tRPC заключается в том, что он позволяет абстрагироваться от базовой инфраструктуры вашей распределенной системы, упрощая добавление, удаление и масштабирование серверов и служб. Маршрутизатор tRPC также может предоставлять дополнительные функции, такие как балансировка нагрузки, маршрутизация на основе данных запроса и обнаружение служб.
В нашем случае мы хотим настроить маршрутизатор с операциями CRUD для приложения TODO. Давайте рассмотрим это шаг за шагом.

Сначала мы импортируем initTRPC
из @trpc/server
и z
из zod
. Затем мы используем метод initTRPC.create()
для создания экземпляра tRPC. Затем мы используем этот экземпляр для создания publicProcedure
и router
.
После создания этих констант мы добавили несколько todos
в качестве начального состояния. Затем мы создаем маршрутизатор (подробнее об этом в следующем шаге) и, что не менее важно, мы экспортируем тип нашего todosRouter
. Позже это станет очень важным, когда мы начнем вызывать методы из нашего клиента.
Напишем несколько маршрутов. Давайте сначала начнем с маршрута, который возвращает наши todos
.

Это выглядит очень просто, правда? Для классических вызовов GET
мы используем .query
метод, доступный в объекте publicProcedure
. Мы передаем простую функцию обратного вызова в функцию .query
, которая возвращает наши todos
.
Давайте посмотрим на методы, которые обновляют наши todos. Скажем, функция, которая добавляет todo.

Мы добавили новый ключ с именем addTodo
на объект, мы передаем в функцию router
. На этот раз мы использовали вызовы .input
и .mutation
в publicProcedure
.
Метод .input
позволяет нам проверить параметры функции. Для этого мы используем zod
.
Затем мы можем использовать функцию .mutation
в сочетании с преобразователем для реализации логики добавления нового Todo. Важно знать, что параметр функции доступен как свойство с именем input
для объекта, переданного нашему распознавателю. Мы можем понять это с помощью реструктуризации ({input}
).
Мы можем завершить наш маршрутизатор, добавив недостающие операции CRUD.

Мы успешно внедрили сервер fastify с маршрутизатором tRPC. Давайте теперь переключимся на сторону Angular и вызовем наши удаленные функции.
Клиент tRPC
Бэкэнд-вызовы Angular выполняются внутри службы с использованием HTTPClient
Angular. Вот пример службы для приложения Todo, которое вызывает несколько конечных точек REST.

Но мы не хотим правильно вызывать конечные точки REST; мы хотим вызывать некоторые удаленные функции, используя tRPC. Итак, давайте установим пакет @trpc/client
и проведем рефакторинг нашего сервиса.
npm i @trpc/client
Для начала нам нужно создать экземпляр клиента. Для этого мы будем использовать две вспомогательные функции с именами createTRPCProxyClient
и httpBatchLink
, предоставленные @trpc/client
.

Самое главное здесь — это дженерик, который мы передаем в createTRPCProxyClient
. Но откуда взялся TodosRouter
?
Помните, мы упоминали ранее, что эта строка в нашем бэкенде очень важна?
export type TodosRouter = typeof todosRouter;
Это типы, которые мы хотим импортировать в наш интерфейс. Итак, давайте добавим следующий импорт в нашу клиентскую службу.

С этой настройкой tRPC выполнит свою магию и обеспечит максимальную безопасность типов.
Пора вызывать некоторые функции. Давайте начнем с получения наших Todos.
Функции запроса вызова

Мы можем использовать наш клиент для вызова функции .todos.query
. Вызов этой функции возвращает Promise. Мы можем использовать fromProise
, чтобы преобразовать его в Observable, чтобы он хорошо вписался в Angular и был похож на использование HTTPClient
.
Лучше всего то, что мы получаем эпическую поддержку IDEA благодаря магии tRPC.

IDEA знает обо всех доступных удаленных функциях внутри нашего внешнего интерфейса. Функция todos
возвращает только todos
и не принимает никаких входных данных. Давайте посмотрим, работает ли безопасность типов и для таких мутаций, как addTodo
.
Вызов mutations
Вызов мутаций аналогичен вызову функций .query
. Чтобы добавить новую задачу, мы вызываем функцию .addTodo.mutate
и передаем нашу новую задачу.

Что, если мы передадим string
вместо объекта todo
?

Мы получаем дружественное предупреждение, поскольку тип string
нельзя присвоить объекту типа {todo: string, done: boolean}
. Полная безопасность типов!
Резюме
tRPC — выдающаяся технология. Это позволяет вам вызывать бэкэнд-функции из вашего внешнего интерфейса. Используя tRPC, вы используете всю мощь TypeScript во всем стеке.
tRPC гарантирует, что ваш внутренний и внешний интерфейс никогда не будут рассинхронизированы. Контракт заключен на TypeScript, без генерации кода, только на TypeScript.
Конечно, вы можете использовать tRPC только в том случае, если ваш бэкэнд написан на TypeScript, а код бэкенда находится в том же репозитории, что и ваш код внешнего интерфейса.