every Tech Blog

株式会社エブリーのTech Blogです。

ネットスーパーアプリにおける GraphQL Mesh を利用した Gateway Server について

はじめに

DELISH KITCHEN 開発部で小売向き合いの開発をしている池です。 この記事は every Tech Blog Advent Calendar 2023 の 19 日目です。

本記事では、弊社が提供しているネットスーパーアプリにおける、GraphQL Mesh を利用した GraphQL Gateway Server について、紹介したいと思います。

構成について

ネットスーパーアプリでは複数サーバーからデータを連携して取得することを想定し、Gateway 構成を採用しています。

例えば、ネットスーパーで保持している商品情報をもとに、関連する情報を他サーバーから取得し、Gateway で集約してアプリに返却するイメージです。

バックエンド API として、GraphQL 形式のネットスーパーAPIと、接続予定の REST 形式の他サーバーAPIがあり、Flutter ネットスーパーアプリはそれらを統合する Gateway Server を経由してデータを取得しています。

この構成において、Gateway Server として GraphQL Mesh というライブラリを利用しており、ここからは GraphQL Mesh の選定理由から、普段の開発時における活用事例について紹介します。

GraphQL Mesh 選定理由

上記構成の特徴として、バックエンド API の形式が異なるということがあげられます。ネットスーパー API は 株式会社ベクトルワン様からの事業譲渡により引き継いだシステムで、GraphQL 形式の API として作成されていました。それに対し、他サーバー API は REST 形式となっています。そのため、アプリはそれら異なる形式の API から組み合わせてデータを取得する必要があります。

そのような特徴を踏まえて、技術選定する上での重要な要件をまとめると次のとおりです。

  • 様々な形式( REST , GraphQL など)のバックエンド API をサポートしていて、それらをスキーマ統合できる
  • スキーマ統合を Gateway 側で完結できる(バックエンド側の変更不要)

2 点目は開発しやすさを重視した要件として設定しています。 その他にも、ドキュメントや事例が豊富にあるか、ライブラリのメンテナンスが頻繁に行われているか、Github のスター数なども考慮しつつ選定を行いました。

比較検討したライブラリは次の 4 つです。

この中で唯一すべての要件にマッチしたのが GraphQL Mesh だったので、採用に至りました。

Apollo Federation は最も活用事例が豊富でドキュメントも整っていましたが、当時 Apollo Federation に準拠した GraphQL のみがバックエンド API として接続可能であったことと、スキーマ統合する場合にバックエンド側で設定が必要であったため、要件に合致せず見送りました。

branble と nautilus はどちらも Go 製の GraphQL federation gateway ということもあり、比較検討対象のライブラリとしましたが、これらも GraphQL のみ対応でした。さらに、実例も少なかったため、採用にはなりませんでした。

GraphQL Mesh 活用事例

ここからは GraphQL Mesh の導入手順と、普段 GraphQL Mesh をどのように活用して開発を行っているか、紹介します。

  • GraphQL Mesh 導入
  • Envelop ライブラリを用いたプラグイン構築

GraphQL Mesh 導入

まずは、導入手順です。スキーマ統合を考慮せずにバックエンドに接続するのみであれば簡単に導入することができます。

ライブラリのインストール

GraphQL Mesh のライブラリをインストールします。

npm i @graphql-mesh/cli @graphql-mesh/graphql graphql

設定ファイルに接続情報を記載

バックエンドサーバーの接続情報を yaml 形式で記述します。この例は GraphQL 形式のバックエンドに接続する記述になります。 実際には、API KEY などの機密情報は Github Actions で AWS Secret Manager から取得して環境変数に設定するようにしています。

sources:
  - name: Fresh API
    handler:
      graphql:
        endpoint: ${FRESH_API_ENDPOINT}
        introspection: ./fresh-schema.graphql
        schemaHeaders:
          Content-Type: application/graphql
          x-api-key: ${SERVER_API_KEY}
        operationHeaders:
          Content-Type: application/graphql
          x-api-key: ${SERVER_API_KEY}
        method: POST

起動

次のコマンドで GraphQL Mesh サーバーを起動できます。これらコマンドを npm scripts に設定して実行しています。

mesh build
mesh start

サーバーの起動に成功すると、サーバーのドメインにブラウザからアクセスすることで playground を利用できます。

Envelop を用いたプラグイン構築

Envelop とは GraphQL 実行時のレイヤーをカスタマイズするためのライブラリです。Envelop を用いることで GraphQL サーバーを強化するプラグインを簡単に構築、構成することができます。

具体的には、GraphQL の実行フロー parse、validate、execute、subscribe などの前後のフェーズにフックして処理を実行することが可能になります。

弊社 Gateway Server では Envelop を利用して以下のような独自のプラグインを作成して、ログの取得や、ハンドリング等を行っています。

  • アクセスログ
  • エラーログ
  • エラーハンドラー
  • Sentry へのログ送出
  • アプリ強制アップデート判定用のハンドラー
  • サーバーメンテナンス用のハンドラー

Envelop 利用する手順と、エラーログを取得する一例を説明します。

導入

Envelop ライブラリをインストールします。

npm i @envelop/core graphql

処理の記述

続いて、エラーログ取得の処理です。 GraphQL の execute フェーズの前後にフックしてエラーログをターミナルに出力する処理を記述します。

export function useErrorLogger(): Plugin<HttpRequestContext> {
  return {
    onExecute({
      args: {
        contextValue: { req },
        operationName,
      },
    }) {
      const startNs = process.hrtime.bigint();

      return {
        onExecuteDone: ({ result }) => {
          // NOTE: AsyncIterable in case of stream response.
          if (isAsyncIterable(result)) return;
          if (result.errors === undefined) return;

          for (const e of result.errors) {
            const errorlog = {
              log_type: "error",
              message: e.message,
              locations:
                e.locations
                  ?.map((l) => `{ line: ${l.line}, column: ${l.column} }`)
                  .join("") ?? "",
              path: e.path?.join(",") ?? "",
            };
            logger(errorlog);
          }
        },
      };
    },
  };
}

Envelop に追加

最後、作成した処理を Envelop に追加し、yaml に読み込むための設定を記述します。

type EnvelopPlugins = Parameters<typeof envelop>[0]["plugins"];

export default function (): EnvelopPlugins {
  return [useErrorLogger()];
}
additionalEnvelopPlugins: ./plugins

以上により、Gateway Server に API リクエストを投げると、GraphQL 実行時の前後でエラーが作成した処理が動作するようになります。

エラーログ出力例

{
    "log_type": "error",
    "timestamp": 1647921567193,
    "message": "Cannot return null for non-nullable field XXXXXXX.xxxxxxxx.",
    "locations": "{ line: 7, column: 7 }",
    "path": "xxxxxxxxxxxxxxxx,xx,xxxxxxxxxxx"
}

所感

良かった点 / 使いづらい点

まだ当初想定していた異なる形式の複数バックエンド API をスキーマ統合することについて、まだテスト的な動作検証しかしていないため、本来の所感はそれ以降かと思っていますが、現状運用してきた中でも以下の点を良かったと感じています。

  • カスタマイズ性の高さ
  • 機能の豊富さ
  • コミュニティが活発

紹介した Envelop だけでなく、スキーマ開発用モック追加、カスタムリゾルバを使用したスキーマ統合、など多くの機能を有しており、現状問題なく開発できています。 コミュニティが活発なので、困った場合は Github の Issue などを検索すると、大体同じ内容の課題を調べることができました。

反対に以下については使いづらさを感じました。

  • カスタマイズの複雑さ
  • デバッグの難しさ

カスタマイズ性の高さや機能の豊富さの反面、それぞれが GraphQL Mesh 独自の記載方法になるため、理解して適用するまでには一定の時間を要します。 また、GraphQL Mesh に限ったことではないですが、GraphQL Mesh 内で特定の GraphQL 実行レイヤーにおける特定のエラーを出したい場合に再現が難しく、デバッグに困ることがありました。

今後の課題

今後の課題はたくさんありますが、一例です。

  • スキーマファイルのバックエンド/フロント間における管理
  • キャッシュ戦略

現状、アプリ / Gateway / バックエンド 間でスキーマファイル自体を受け渡して追従しているため、二重三重管理となっています。本来は一つの共通のスキーマファイルをそれぞれのレイヤーで扱うことが理想だと思います。 また、現状 GraphQL Mesh のキャッシュ機能を利用しておらず、ネットスーパーのバックエンド API のパフォーマンスも低いため、適切なキャッシュ戦略を検討して適用することも課題です。バックエンド API にも関連しますが、Persisted Query なども検討できれば、よりパフォーマンス改善に繋がると考えています。

おわりに

GraphQL Mesh を利用した Gateway Server について選定理由と活用事例、所感を記載させていただきました。

GraphQL Mesh は紹介した機能の他にも多くの機能があり、ドキュメントも豊富であるため、様々なカスタマイズを簡単に実現することが出来ます。 現状はネットスーパーのバックエンドにのみ接続していますが、今後の取り組みとして他のサーバーと接続して、ネットスーパーの商品情報と関連した情報と連携させていく予定です。 その際にスキーマ統合の実装が加わるので、また次回その内容も紹介できればと思います。

以上、どなたかの参考になれば幸いです。