API Gateway является одним из обязательных компонентов архитектуры современных систем. Он может решать различные задачи: Проксирование вызовов из одной точки в разные сервисы в разных форматах. Контроль доступа к ним. Мониторинг. Аутентификация и авторизация. Агрегация данных (один запрос к API Gateway - несколько запросов к бэку). Управление таймаутами. Кэширование данных. Валидация. И многое другое. При отсутствии API Gateway все эти задачи ложатся на бэкенд. И если для монолита это может быть оправдано, то в микросервисах любое изменение общей логики повлечёт доработку всех зависимых сервисов. Есть много интересных готовых решений, в т.ч. и для нашего кейса (GraphQL <-> gRPC), но ни одно из них не подошло нам в полной мере. На нашем проекте API Gateway нужен в основном для запросов с фронта. Когда мы только начинали, основным видом взаимодействия был REST. Он достаточно прост для понимания и реализации, но в процессе развития проекта начали проявляться его недостатки: При обновлении сущности, даже если обновить нужно пару полей, мы обновляли весь объект. Да, можно использовать метод PATCH и парсить на бэке только переданные поля, рассматривая отдельно ситуации, когда поле передано со значением null и когда поле не передано, но это не стандартный подход для REST. Не всегда при запросе на получение сущности нужны все поля. Можно сделать несколько методов вместо одного, но тогда для каждого кейса придётся делать отдельный метод. А если для одной и той же структуры у пользователей с разными ролями будут разные доступы к полям, которые, к тому же, могут меняться, реализовать это, используя REST, невозможно. Нет пакетной обработки. Для получения данных из каждого сервиса фронт делает отдельные запросы. GraphQL, хоть и имеет свои недостатки, успешно решает эти проблемы. В качестве бонуса в GraphQL в отличие от REST есть директивы. Их использование даёт возможность делать свои обработчики для полей запроса, императивно добавлять кэширование, валидацию и прочее. С API для фронта разобрались, переходим на бэк. Проблемы остаются те же, за исключением пакетной обработки (на бэке таких кейсов нет и не предвидится). Можно изначально делать API бэка на GraphQL, это бы значительно упростило создание API Gateway. Но не всё так просто.  Мы пишем на Java, используем Quarkus, и для работы с GraphQL там есть неплохие на первый взгляд решения как для серверной части, так и для написания клиентов. С серверной частью проблем нет, но клиентская далека от совершенства. Typesafe-клиент очень прост и минималистичен.  Однако такой клиент не даёт возможности делать частичное обновление или выбирать поля для формирования ответа. Dynamic-клиент очень гибкий. Можно передавать какой угодно набор полей, можно использовать переменные, можно задавать список полей, которые мы хотим получить в ответе.  Но это очень громоздко. К тому же при формировании запроса можно допустить ошибку. В gRPC есть возможность генерировать типобезопасные контроллеры и клиенты. А для ограничения списка полей, которые возвращаются в ответ, есть специальный тип - FieldMask. В gRPC в качестве формата запросов-ответов используется protobuf. Его же мы используем для сообщений в асинхронном взаимодействии через kafka. Т.о. нам не нужно плодить форматы, и мы можем использовать на бэке что-то одно. Часто, когда говорят о преимуществах gRPC, упоминают возможность потоковой передачи данных, снижающей накладные расходы. Мы к этому пока не пришли, но хотелось бы иметь такую возможность в будущем. В protobuf есть проблема с передачей null-значений, но её мы решили по-своему (об этом будет написано далее). Ранее я писал, что на бэке мы используем Java в качестве основного языка программирования, поэтому хотелось бы подобрать готовое решение именно на нём. Единственное, что удалось найти, это проект Rejoiner. Выглядит интересно, но это не коробочное решение, а именно библиотека. К тому же для каждого эндпоинта нужно писать свой обработчик, что не очень удобно, т.к. при его добавлении/изменении придётся пересобирать проект. Ещё один минус - проект последнее время не развивается. Однако именно в его исходниках удалось почерпнуть много полезной информации для создания собственного API Gateway. Ещё один интересный проект - GraphQL Mesh - написан на TypeScript и предоставляет возможность маппинга из GraphQL не только в gRPC, но и используя другие способы взаимодействия. Но он не даёт возможности кастомизировать маппинг типов и добавлять свои обработчики для запросов. Всё это можно было бы сделать самим, но пишем мы на Java. Есть решения на Go, но они предполагают генерацию API Gateway из контракта, что нас не устраивает. В итоге было принято решение написать свой API Gateway на нашем стэке технологий. Список основных требований к концепту: Сервис должен быть написан на Java/Quarkus. В сервисе не должно быть кодогенерации DTO и стабов, которые необходимо использовать для вызова сервисов, чтобы не пришлось пересобирать проект каждый раз после изменения API какого-либо вызываемого сервиса. В сервисе должна быть возможность "горячего обновления" контракта. При изменении сервисов на бэке не должно быть необходимости править код API Gateway. Поддержка batch-запросов.  quarkus-... - зависимости фреймворка для работы с YAML-конфигом и gRPC.graphql-java-extended-scalars - добавляет скаляры GraphQL. Из коробки поддерживается не так много типов, поэтому без использования этой зависимости не обойтись.graphql-java-extended-validation - добавляет директивы для валидации запросов.protobuf-java-util - нужно для логирования запросов-ответов от gRPC-сервера в формате JSON.jackson-databind - нужно для корректного логирования в формате JSON.lombok - перчик.vertx-web-graphql - нужно для конфигурации серверной части GraphQL с помощью... Vertx. Хотя Quarkus и является основным фреймворком для приложения, но он, к сожалению, больше заточен на подход code-first, а мы строго придерживаемся подхода contract-first. К тому же при подходе code-first нам бы не удалось реализовать какой-то универсальный обработчик. Поэтому фактически от Quarkus в проекте будет только DI, конфигурация и запросы к gRPC-сервисам. А для создания и настройки основного обработчика больше подходит Vertx. У Quarkus под капотом находится Vertx, что бывает очень полезно для написания каких-то низкоуровневых вещей. Серверная часть Quarkus полностью основана на Vertx, реактивные REST/GraphQL клиенты под капотом используют HTTP-клиент из Vertx, работа с БД идёт через библиотеки Vertx. Однако не все компоненты, для которых есть реализация в Vertx, переиспользуются в Quarkus. Серверная часть для GraphQL и обёртка для работы с gRPC в Quarkus написана без использования Vertx. Как сконфигурировать серверную часть GraphQL, можно посмотреть в документации Vertx и документации graphql-java. В итоге у меня получилась такая конфигурация:  В основном методе выполняются следующие действия: парсится схема graphql добавляются обработчики аннотаций (в данном случае это SizeConstraint из библиотеки graphql-java-extended-validation, который обеспечивает обработку директивы @Size) добавляются обработчики для методов Query и Mutation (в нашем случае это один обработчик для всех методов) добавляются скаляры GraphQLLong, DateTime, Date для поддержки соответствующих типов добавляется Instrumentation для логирования.   Тут стоит обратить внимание на .setRequestBatchingEnabled(true). За счёт этого параметра мы делаем возможным выполнение batch-запросов к API Gateway.  У меня роутинг зашит прямо в конфигурационном файле приложения. Это оптимальное решение для концепта, но для реального рабочего проекта лучше использовать централизованное хранилище (БД, внешний кэш), т.к. при большом количестве сервисов конфигурационный файл будет очень громоздким и нечитаемым.    Подробнее про конфигурацию Quarkus-приложения можно почитать в документации. Для простоты я не делал маппинг методов GraphQL в методы gRPC и считаю, что у они у нас будут называться одинаково. Т.о. мне остаётся лишь смаппить метод GraphQL в сервис gRPC. Proto - бинарный формат со строгой типизацией. Поэтому для формирования запроса и парсинга ответа необходимо иметь у себя соответствующий контракт, на основе которого генерируются DTO запросов/ответов и стабы. С учётом того, что контракты gRPC-сервисов лежат в них самих, получать их нужно именно оттуда. Примеров не так много, я нашёл такой, и выглядит он страшновато. В нём нас интересует метод lookupService. После небольшого рефакторинга у меня получилось следующее:   serviceName - полное название сервиса, которое я получаю из конфига, например ru.craftysoft.platform.grpcservice.proto.GrpcService. В коде клиента стоит обратить внимание на метод io.quarkus.grpc.runtime.ClientCalls.oneToOne(), возвращающий ответ, обёрнутый в io.smallrye.mutiny.Uni. В Quarkus активно используется библиотека smallrye-mutiny, а Uni - это одно из представлений результата, аналог Mono из project-reactor. Из полученного ответа нам требуется извлечь дескриптор сервиса, в котором хранится информация о сущностях и методах. В ответе сервиса приходит com.google.protobuf.DescriptorProtos.FileDescriptorProto в бинарном виде. Из него необходимо получить информацию о контракте с учётом всех его зависимостей, т.к. контракт может содержать информацию из разных файлов, включая proto-файлы из стандартной библиотеки и импортированных библиотек. Маппинг JSON-запроса в запрос к gRPC-сервису я подсмотрел в проекте Rejoiner, который упоминал ранее, и добавил туда маппинг классов-обёрток для nullable-типов. В protobuf нельзя передать null-значение. В отдельных случаях можно использовать стандартные классы-обёртки, которые есть в библиотеке, такие как StringValue, BooleanValue, рассматривая их значение по умолчанию как null. Но если для строк (в нашем случае) это будет работать, т.к. значением по умолчанию там является пустая строка, то 0 для числовых значений для нас неприемлем. Я подготовил небольшой proto-файл с обёртками для примитивов, в котором используется google.protobuf.NullValue для корректной работы с null. Ограничения маппинга: Нельзя смаппить запрос, принимающий более одного параметра. Нет маппинга для oneof. Исключением являются кастомные nullable-типы. Для перечислений на стороне gRPC сервисов необходимо задавать какое-то неиспользуемое значение по умолчанию, которое можно принимать за null. Нет маппинга для map<key, value>, потому что в GraphQL нет аналога. Не то, чтобы я часто пользовался этим типом в gRPC, но в паре REST-методов он есть. Маппинг запрашиваемых полей в FieldMask возможен, только если FieldMask лежит в корне структуры в proto. В дальнейшем это будет исправлено. Кроме самого запроса необходимо подготовить дескриптор метода, который включает в себя тип запроса (синхронный запрос, стриминг), название метода и маршаллеры запроса/ответа.  Запрос делается аналогично запросу на получение дескриптора сервиса. В самом простом случае для преобразования ответа в JSON можно было бы использовать метод com.google.protobuf.util.JsonFormat.printer().print(MessageOrBuilder message), но там нет и быть не может нормального маппинга nullable-значений, даты и даты-времени. В остальном маппинг в JSON достаточно прост и представляет собой рекурсивный обход ответа и преобразование его в Map<String, Object>. Готовый проект можно посмотреть на Github. Он включает в себя API Gateway, небольшой gRPC-сервис и библиотеку с nullable-типами. Несмотря на имеющиеся ограничения, этот проект может быть основой полноценного API Gateway. Одним из требований к приложению было наличие возможности перезагрузки контракта без остановки приложения. Это не имеет смысла, если хранить контракт в самом приложении (как сейчас), но если держать его в БД/кэше/хранилище схем, функция очень полезная. У меня это реализуется за счёт пересоздания io.vertx.ext.web.Route.   Новый контракт становится актуальным сразу после загрузки. Изначально на каждый запрос с фронта делался запрос на получение дескриптора. И хотя выполняется он достаточно быстро, нет смысла делать его каждый раз, т.к. контракт меняется нечасто. Поэтому ответ можно закэшировать. Для кэширования я взял библиотеку quarkus-cache, использующую Caffeine.     Несмотря на то, что кэшируемый метод возвращает Uni, кэшируется именно результат. При повторном запросе по тому же ключу запрос в gRPC-сервис за дескриптором уже не делается. Если возвращается ошибка при запросе к дескриптору, результат не кэшируется.  Умный, красивый, скромный