AWS AppSyncとApollo ClientでGraphQLのSubscriptionを実装する

2024/12/09に公開

AWS AppSyncで作成したGraphQL APIに対して、Apollo ClientからSubscriptionするやり方を調べました。最低限のセットアップではありますが、備忘録として記事にまとめようと思います。

前提

Viteで立ち上げたReact × TypeScriptのアプリケーションで、型安全なSubscriptionを実装するところまでをゴールとします。一連の流れをおさえるための内容になりますので、細かい手順や技術そのものには触れません。あらかじめご了承ください。

大まかに以下の手順で進めていきます。

  1. AppSyncの設定
  2. Apollo Clientの実装
  3. GraphQL Codegenの設定
  4. GraphQL Subscriptionの実装

GraphQLスキーマ・APIの作成 〜 スキーマファイルの配置

はじめにAWS マネジメントコンソール上でスキーマとAPIを作成します。手順は省略しますが、AWSのチュートリアルを参考にすれば問題ないはずです。

作成が完了すると以下のスタックができあります。

  • スキーマ
  • API
  • リゾルバ
  • データソース (DynamoDBのテーブル)

続いて、スキーマファイルをダウンロードしてプロジェクト内に配置します。 schema.graphql はコードを生成の材料となるため、Graphql Codegenから参照できるプロジェクト内に保存しました。

# プロジェクトのルートディレクトリにて
mkdir graphql/{schemas,documents}
mv ~/Downloads/schema.graphql graphql/schemas

AppSyncでスキーマを作成すると、AWS独自のスカラーが使用されます。このままではコード生成時にエラーが出てしまうため、aws.graphql を作成してDateTime型のスカラーを定義してあげます。

@/graphql/schemas/aws.graphql
# https://github.com/dotansimha/graphql-code-generator/discussions/4311#discussioncomment-2752096
scalar AWSDateTime

Apollo Clientの設定 〜 オペレーションの追加

こちらのリポジトリを参考にして、Apollo V3とAppSyncとの連携に必要なパッケージをインストールします。

npm install @apollo/client \
graphql \
aws-appsync-auth-link \
aws-appsync-subscription-link

認証に必要な情報はAWS マネジメントコンソールから確認できます。「Real-time endpoint」と「GraphQL endpoint」の2つがありますが、HTTPの方である「GraphQL endpoint」を設定します。

@/lib/apollo/client.ts
import { ApolloClient, ApolloLink, InMemoryCache } from "@apollo/client";
import { AuthOptions, createAuthLink } from "aws-appsync-auth-link";
import { createSubscriptionHandshakeLink } from "aws-appsync-subscription-link";
import { UrlInfo } from "aws-appsync-subscription-link/lib/types";

// APIキーで認証
const auth: AuthOptions = {
  type: "API_KEY",
  apiKey: import.meta.env.VITE_AWS_APPSYNC_API_KEY,
};
const urlInfo: UrlInfo = {
  // URLはWebsocketではなく、HTTPの方を使う
  url: import.meta.env.VITE_AWS_APPSYNC_HTTP_URL,
  region: import.meta.env.VITE_AWS_APPSYNC_REGION,
  auth,
};

const link = ApolloLink.from([
  createAuthLink(urlInfo),
  createSubscriptionHandshakeLink(urlInfo),
]);

const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
});

export { client };

続いて、APIへのリクエストとなるオペレーションを追加します。ここで定義したものがApollo Clientのフックとしてコード生成されます。schema.graphql で定義されたデータ構造を確認しながら、デモに必要なQueryとSubscriptionだけを用意しました。

@/graphql/documents/todo.ts
import { gql } from "@apollo/client";

export const GET_TODOS = gql`
  query listTodos {
    listTodos {
      items {
        id
        title
        who
        when
        description
      }
    }
  }
`;

export const ON_CREATE_TODO = gql`
  subscription onCreateTodo {
    onCreateTodo {
      id
      title
      who
      when
      description
    }
  }
`;

GraphQL Codegenの設定 〜 コード生成

GraphQL Codegenはプラグインを切り替えることで、様々な形式のファイルを出力できます。ここではTypeScriptの型とApollo Clientのフックがほしいので、プラグインと併せてインストールします。

npm install -D @graphql-codegen/cli \
@graphql-codegen/client-preset \
@graphql-codegen/introspection \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo

初期化コマンドを実行してプロンプト上でいくつかの質問に答えると、設定ファイル codegen.ts が生成されます。

npx graphql-code-generator init

? What type of application are you building? Application built with React
? Where is your schema?: (path or url) graphql/schema.graphql
? Where are your operations and fragments?: graphql/documents
? Where to write the output: graphql/generated.ts
? Do you want to generate an introspection file? No
? How to name the config file? codegen.ts
? What script in package.json should run the codegen? codegen
Fetching latest versions of selected plugins...
codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: "graphql/schemas",
  documents: "graphql/documents",
  generates: {
    "graphql/generated.ts": {
      preset: "client",
      plugins: []
    }
  }
};

export default config;

プラグインを手動で設定します。configマニュアルを見ながらはお好みで設定しましょう。

codegen.ts
@@ -6,10 +6,23 @@ const config: CodegenConfig = {
   documents: 'graphql/documents',
   generates: {
     'graphql/generated.ts': {
-      preset: 'client',
-      plugins: [],
+      plugins: [
+        'typescript',
+        'typescript-operations',
+        'typescript-react-apollo',
+      ],
+      config: {
+        withHooks: true,
+        skipTypename: true,
+        enumsAsConst: true,
+        avoidOptionals: true,
+        defaultScalarType: 'unknown',
+      },
     },
   },
+  hooks: {
+    afterAllFileWrite: ['prettier --write'],
+  },
 };

 export default config;

コード生成の準備ができました。初期化コマンドによって package.jsoncodegen スクリプトが追加されているので、これを実行して generated.ts が生成されたことを確認します。

npm run codegen
@/graphql/generated.ts
// ...

/**
 * Todoモデルの型
 */
export type Todo = {
  description: Maybe<Scalars['String']['output']>;
  id: Scalars['ID']['output'];
  title: Scalars['String']['output'];
  when: Scalars['AWSDateTime']['output'];
  who: Array<Maybe<Scalars['String']['output']>>;
};

// ...

/**
 * Todoの生成をSubscriptionするReactフック
 */
export function useOnCreateTodoSubscription(
  baseOptions?: Apollo.SubscriptionHookOptions<
    OnCreateTodoSubscription,
    OnCreateTodoSubscriptionVariables
  >,
) {
  const options = { ...defaultOptions, ...baseOptions };
  return Apollo.useSubscription<
    OnCreateTodoSubscription,
    OnCreateTodoSubscriptionVariables
  >(OnCreateTodoDocument, options);
}

QueryとSubscriptionの実装

最後にApollo Clientのプロバイダーを設定して...

main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { client } from "./lib/apollo/client";
import { ApolloProvider } from "@apollo/client";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </StrictMode>
);

生成されたフックを呼び出すだけです...!!

App.tsx
import { useEffect } from 'react';
import {
  useListTodosQuery,
  useOnCreateTodoSubscription,
} from '../graphql/generated';

function App() {
  const { data: todoListData, loading, client } = useListTodosQuery();
  const { data: todoSubscriptinoData } = useOnCreateTodoSubscription();

  // Subscriptionしているデータが更新されたら、キャッシュを再検証して最新のTodoを取得する
  useEffect(() => {
    client.resetStore();
  }, [todoSubscriptionData, client]);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!todoListData?.listTodos?.items) {
    return null;
  }

  console.log({ data });

  return (
    <>
      <ul>
        {todoListData.listTodos.items.map((todo) => (
          <li key={todo?.id}>
            <span>{todo?.title}</span>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;

おわりに

コードをほとんど書かずにGraphQL Subscriptionの実装ができてしまいました。マネージドサービスであるAppSyncやコード生成ツールであるCodegenを活用することで、開発を効率的に進められそうです。

https://github.com/yuki-yamamura/learn-appsync

frontend flat

Discussion