За последние пару лет было много шумихи вокруг GraphQL и gRPC. В Attest всякий раз, когда мы создаем продукты, предназначенные для внутреннего использования, у нас гораздо более высокий аппетит к риску, и мы считаем, что это прекрасные возможности поиграть с новыми технологиями, которые мы могли бы однажды использовать для наших конечных точек, ориентированных на клиентов. Мы заметили, что и gRPC, и GraphQL основывают свои основные принципы на подходе проектирование по контракту, и подумали, что это прекрасная возможность протестировать эти технологии на одном из наших внутренних API.

В этой статье мы стремимся продемонстрировать, как нам удалось получить полностью сквозной типизированный API, написанный на TypeScript, путем генерации типов для любого конца приложения. Мы также обсудим, как мы можем использовать эти типы на любых интерфейсных клиентах, трудности, с которыми мы столкнулись, любые странности, которые мы обнаружили, и каков наш опыт до сих пор. Наконец, мы рассмотрим плюсы и минусы выбора TypeScript.

Предпосылки для статьи

Прежде чем читать дальше, убедитесь, что вы понимаете основы gRPC, TypeScript и GraphQL. Следующие ссылки на документы и статьи могут быть полезны:

Документация

Статьи

Архитектура приложения

Первым шагом было определение структуры нашего приложения. На нашу архитектуру повлияли два основных фактора: возможность легко:

  • Замена клиентов RPC на клиенты REST без изменения преобразователей (контроллеров), служб или любых связанных моделей / преобразователей.
  • Поменяйте GraphQL на простой REST API без изменения наших клиентов, служб или любых связанных моделей / преобразователей.

Мы закончили тем, что следовали шаблону сервисного уровня, чтобы иметь максимально независимый от протокола подход:

Каждый из показанных выше слоев состоит из своих собственных моделей, преобразователей и ошибок, причем двустороннее преобразование модели выполняется на родительском уровне, что позволяет каждому уровню быть независимым от потребителя. Решение иметь представление модели на каждом уровне было вызвано необходимостью правильно разделить проблемы, что упростило поддержку кода, сделало его более тестируемым и многоразовым. Стоимость выполнения - это многословие, небольшая плата за то, что мы считаем большой победой, как если бы мы хотели, чтобы наш клиентский уровень жил в отдельном репо, чтобы его использовал другой проект - это было бы чрезвычайно просто сделать.

Учитывая приведенную выше диаграмму, структура каталогов была представлена ​​в аналогичной комплексной структуре:

Уровень API

Уровень API (GraphQL) отвечает как за определение конечных точек, так и за их маршрутизацию на соответствующие преобразователи (контроллеры). Мы используем Express в качестве основы для нашего приложения и добавляем Apollo Server в качестве промежуточного программного обеспечения, чтобы помочь с маршрутизацией и интерпретацией нашей схемы GraphQL. Основными причинами, по которым мы выбрали Apollo Server, были инструменты разработчика:

  • Для потребителя API издеваться над одним или несколькими ответами невероятно легко. Это означает, что классический спор о том, должны ли мы создавать E2E или интеграционные тесты, прекращается еще до того, как он начнется. Интеграционные тесты становятся нормой, поскольку простой логический флаг высмеивает ответы API за вас.
  • GraphQL Playground встроена, что упрощает выполнение запросов и внесение изменений в API. Думайте об этом как о Почтальоне (или REST-клиенте), встроенном в конечную точку, которая знает определение API, проверяя и помогая с синтаксисом запроса, прежде чем вы даже попытаетесь его выполнить.

Каталог схемы содержит определение для всех наших моделей API и конечных точек и определяется в соответствии с определением модели схемы, описанным в документации Apollo.

Уровень обслуживания

Наш уровень обслуживания содержит основную логику, используемую в нашем приложении. Этот уровень (используемый преобразователями, определенными на уровне API) взаимодействует с различными клиентами, где он собирает и отправляет соответствующие данные между клиентами и преобразователями.

Этот уровень жизненно важен, поскольку он связывает и управляет несколькими клиентами, изолируя детали клиентских API-интерфейсов от преобразователей. Это позволяет преобразователям быть максимально простыми, клиентам сосредоточиться на своих нисходящих микросервисах, а уровень сервиса объединять все вместе - это означает, что замена клиента gRPC на HTTP не влияет на сервис. слой, и замена логики метода службы не затрагивает резолвер.

Уровень клиента

Клиентский уровень используется для связи с различными микросервисами gRPC или внешними конечными точками HTTP. Axios используется для связи через HTTP и для связи со службами gRPC. Мы используем комбинацию сгенерированных клиентов gRPC и индивидуальную абстракцию с использованием обещаний.

Контекст

Сервер Apollo позволяет добавлять промежуточное ПО между каждым запросом и отправлять через любой контекст, который может вам понадобиться вместе с вашими запросами. Мы используем этот уровень для отправки через JSON Web Tokens (JWT) от внешнего интерфейса к нашим микросервисам:

Как только мы попадаем на наш клиентский уровень, мы конвертируем их в метаданные gRPC, чтобы мы могли передать контекст запроса микросервисам gRPC, где мы можем обрабатывать авторизацию.

Автоматизация через генерацию

После того, как мы полностью определились с архитектурой системы, нам нужно было гарантировать, что оба конца приложения типизированы. Это будет означать, что любые изменения в наших .proto файлах или любых файлах, содержащих теги gql, приведут к ошибкам времени компиляции, а не ошибкам времени выполнения.

Создание типов TypeScript из схемы GraphQL

Одна из самых мощных функций, предлагаемых GraphQL, - это самоанализ. Возможность понять структуру всего API с помощью одного запроса. Добрые люди в Apollo предоставляют библиотеку под названием graphql-tools, которая с учетом конечной точки преобразует схему в эквивалентную интерпретацию JSON.

Если у вас есть файл JSON, вы можете использовать Генератор кода GraphQL вместе с его плагином TypeScript для создания файла определения TypeScript (d.ts) из файла schema.json.

Обратной стороной вышеупомянутых инструментов является то, что они полагаются на работающий экземпляр сервера. Раздражает необходимость помнить о запуске сервера каждый раз, когда мы хотим регенерировать типы; поэтому мы создали (слегка примитивный) сценарий bash для «автоматизации» процесса.

  1. Разверните сервер в фоновом режиме.
  2. Изучите схему и сгенерируйте файл schema.json.
  3. Убейте сервер.
  4. Сгенерируйте определения TypeScript из файла schema.json.

Типы TypeScript генерируются для нас с помощью простого скрипта, приведенного выше. Это означает, что если мы изменим следующее schema.ts:

После определения нашего запроса запуск tools/generate-gql.sh создаст для нас следующий schema.d.ts файл:

Вышеупомянутые типы означают, что мы можем даже вводить наши резолверы:

Использование schema.json во внешнем интерфейсе

Файл schema.json выходит за рамки создания типов серверной части - он может использоваться любыми клиентами, использующими API, для создания своих собственных типов. Это означает, что когда мы меняем наш API, наш клиент (интерфейсное приложение, также написанное на TypeScript) может загружать схему, генерировать типы и обнаруживать любые потенциальные ошибки во время компиляции - выигрыш для обоих API и фронтенд-разработка.

Сравнение инструментов генерации GraphQL TypeScript

Мы нашли два отличных инструмента, позволяющих создавать типизированный сгенерированный код:

Мы решили использовать генератор кода GraphQL, поскольку он генерирует типы преобразователей, а также структуры запросов и мутаций - это означает, что вы не можете изменить существующий запрос / мутацию, не изменяя сопутствующий преобразователь, как это видно из QueryResolvers.FetchChickenResolver, выше.

Создание клиентов и типов gRPC из прото файлов

Хотя мы рассматривали и тестировали использование динамически сгенерированного кода во время выполнения для наших клиентов gRPC, статически сгенерированный код обеспечивал типизацию и все преимущества проверки во время компиляции.

Когда у вас есть protobuf файлы определений API, которые используются в разных службах, трудно делиться этими файлами и поддерживать использование и реализацию API в актуальном состоянии. Эта проблема усугубляется, когда ваши потребители и производители разделены между несколькими репозиториями git. Чтобы исправить это, у нас есть proto репозиторий, который устанавливается как подмодуль git (proto каталог в обзоре каталогов) у потребителей и производителей, что упрощает совместное использование файлов определений API. Здесь статически сгенерированные файлы JavaScript и TypeScript GRfiles являются примером структуры:

.
 ├── proto/
 │   ├── chicken_service/
 │   │   ├── model/
 │   │   |   └── chicken.proto
 │   │   └── chicken_service.proto
 │   └── ...
 └── …

Модели отделены от определений служб и запросов, чтобы обеспечить возможность повторного использования, и каждая служба имеет свой собственный каталог, поэтому мы можем ориентироваться на генерацию кода для каждой службы.

Чтобы сгенерировать наши типы TypeScript, мы решили использовать grpc_tools_node_protoc. Библиотека, созданная одним человеком, которая будет генерировать необходимый код JavaScript и определения TypeScript для создания экземпляров и использования наших клиентов - это было то, что нам было нужно.

Краткое примечание: мы всегда с осторожностью относимся к библиотекам с ограниченной поддержкой, но agreatfool проделал потрясающую работу - у нас было очень мало проблем с использованием этой библиотеки.

Используя это, мы создали небольшой сценарий bash, который генерирует файлы JavaScript и соответствующие d.ts файлы:

Мы запускаем этот сценарий всякий раз, когда в наши proto файлы вносятся изменения - в результате будут созданы следующие файлы:

Что, в свою очередь, позволяет нам общаться с клиентами следующим образом:

Сравнение инструментов генерации gRPC TypeScript

Мы нашли два инструмента, которые позволяли вводить типизированный сгенерированный код:

  • Невероятный grpc-web: инструмент, созданный для связи со службами, реализующими gRPC через Интернет, от внешнего клиента путем создания шлюза-прокси между клиентом и сервером.
  • Grpc_tools_node_protoc _ts: оболочка для grpc_tools_node_protoc, которая генерирует соответствующие d.ts файлы.

Сначала мы оценили grpc-web, поскольку он создан хорошо известной компанией и имеет несколько участников, хотя он предназначен для Интернета. С этим была одна проблема:

Этот пакет поддерживает Node.js, но требует, чтобы на сервере был уровень совместимости gRPC-Web.

Из-за большого объема работы, необходимой для этого, а также из-за зависимости от каждого из наших производителей, мы не хотели реализовывать этот уровень совместимости для каждого из наших производителей.

Неудача: обработка ошибок в gRPC

gRPC предлагает провайдерам на выбор ряд различных ошибок, которые производитель может выбросить. Однако такие ошибки, как GRPC_STATUS_NOT_FOUND, слишком расплывчаты, чтобы из них можно было извлечь какое-либо конкретное значение - нам нужно знать, что не найдено и почему не найдено.

В наших производителях мы устанавливаем это с помощью свойства details - подумайте о метаданных для ошибок - и добавляем что-то более явное, например CHICKEN_NOT_FOUND_IN_COUP.

На момент написания этой статьи, если были предоставлены и code, и details, при использовании библиотеки Node.js одна была перезаписана другой, то есть мы не могли получить явные типы ошибок без нескольких хаков.

Еще одна причуда заключается в том, что сгенерированная типизация TypeScript устанавливает error type равным any, что означает, что нам нужно явно указать тип ошибки.

Использование TypeScript для создания API

Выбор использования TypeScript для этого проекта над обычным JavaScript предоставил нам все преимущества, которые может дать система со структурной типизацией, и многое другое. Используя TypeScript, мы могли гарантировать, что в случае изменения любого из наших провайдеров наше приложение сломается во время компиляции. Точно так же, если наши конечные точки API сломаются, наши клиентские интерфейсы сломаются во время компиляции. Поэтому мы настоятельно рекомендуем преодолеть трудности, которые могут возникнуть при использовании TypeScript, поскольку выплата намного больше.

Использование TypeScript на более подходящем внутреннем языке, таком как Golang, Java или Rust, имело ряд недостатков (преимущественно на уровне связи gRPC), но также имело несколько важных преимуществ.

Преимущества

  • Apollo Server, который позволил нам сгенерировать драгоценный schema.jsonfile, что, в свою очередь, означало, что все наши конечные точки API во внешнем интерфейсе были типизированы. Это чрезвычайно мощный инструмент, который - при интеграции как в API, так и во внешний интерфейс - может принести огромную пользу обеим командам.
  • Проект был начат небольшой командой, состоящей из 2-х фронтенд-инженеров и 2х бэкэнд-инженеров, что означало, что любой мог перейти к этому внутреннему репозиторию, когда нам потребовались дополнительные руки.

Недостатки

  • gRPC все еще довольно примитивен в JavaScript / TypeScript, что вызвало больше препятствий, чем нам хотелось бы.
  • Такие языки, как Golang или Rust, могли бы быть более производительными, и нашим бэкэнд-инженерам было бы удобнее их писать.

Последние мысли

В заключение мы бы предложили использовать TypeScript вместо JavaScript при написании аналогичных приложений с использованием Node.js. Взвесьте все «за» и «против», если рассматриваете возможность написания аналогичного приложения с использованием TypeScript или более знакомого языка программирования, такого как Golang, учитывая преимущества GraphQL и недостатки gRPC.