AWS初心者がAppSyncを使ってみた

## はじめに
昨年、[ANGEL Dojo 2024](https://aws.amazon.com/jp/blogs/psa/2024-07-angel-dojo-kickoff/)に参加し、3か月間でサービスの企画から開発までを行う経験をしました。

私たちのチームは、電話練習アプリ「Tele Talk Tutor（T3）」を開発しました。
このサービスは、電話対応が苦手な方向けに、営業アポ取りや飲食店アルバイトの予約対応など、リアルな場面での電話練習を提供します。
この電話練習機能にて、AppSyncからLambdaを呼び出す実装を行ったので、その流れを本記事に残しておこうと思います。


**関連記事**
本記事では取り上げていませんが、ユーザの電話対応内容からフィードバックを提供する機能も開発しました。以下の記事でチームメンバーが一部紹介してくれています！

https://qiita.com/Chi_/items/666b38ead5002724de2f


電話練習機能では、ユーザの音声をWebSpeechAPIでテキストに起こし、Lambdaに送信してBedrockで返答内容を生成して返却します。
ここで、**フロントエンドからLambdaを呼び出すため、AppSyncを設計**しています。

:::note info
**AppSyncとは**
AWS AppSyncは、GraphQLを使用してDynamoDBやLambdaなどのデータソースに対してアクセスや操作ができるサービスです。
:::

## AppSyncの設計（GraphQL）
### API作成
まずは、AWS AppSyncコンソールの画面から、GraphQL APIを作成します。
※リソースは独自で設定するよう選択するので、詳細設定は省略

### スキーマ
次に、左メニューからスキーマ設定画面を開いて、パラメータやデータ操作の定義を行っていきます。
Mutation要求の引数で使用する項目を`input`型で定義します。（!マーク：必須項目）
戻り値は`type`を使用して定義します。
```graphql
input SendDataInput {	
	sessionId: String!
	message: String!
}	

type SendDataOutput {	
	audioUrl: String!
}
```
データ操作の定義については、今回の処理は単純なデータ取得（`Query`）ではなく、リクエストデータからのレスポンスデータ作成なので、`Mutation`を使用しています。
```graphql
type Mutation {	
	sendData(input: SendDataInput!): SendDataOutput
}	
```
これでスキーマ定義は完了です。

### データソース
次に、左メニューからデータソース設定画面に行き、以下項目を入力します。
- データソース名
- 説明
- データソース
※今回は「AWS Lambda関数」を選択します。関数は既に作成済みとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941241/46cc0c0f-4264-e714-79d4-0b3beb630155.png)

ここまで選択すると、以下の項目が表示されるため、それぞれ入力します。
- リージョン
- 関数（選択もしくは手動入力）
- ロール（新規もしくは既存から選択）
![hidden_function.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941241/10cbba52-67cf-fb68-99c5-24491074c5e4.png)


### リゾルバー
再度スキーマ画面に戻り、画面右のリゾルバーから、先ほど定義したMutationのアタッチを選択します。（以下画像では、既にアタッチ済みのためボタンは表示されていませんが、設定が完了すると画像の通りになります。）
![enhance_function.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941241/8cd83031-6eef-0712-f670-b96ad43d987f.png)

- リゾルバータイプ
今回は単一のデータソース（Lambda）からの取得になるので、「ユニットリゾルバー」を選択します。
- その他設定（リゾルバ―ランタイム）
「Velocityテンプレート言語(VTL)」を選択します。
- データソース
先ほど設定したものを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941241/fb41ab2f-e5a4-ccbf-3333-9984b32fbf15.png)
作成が完了すると、次の設定画面へ移ります。

### リクエストマッピングテンプレート
ここでは、サンプルテンプレートから「invoke and forward arguments」を選択します。
以下の内容が自動で設定されます。
```
#**
The value of 'payload' after the template has been evaluated
will be passed as the event to AWS Lambda.
*#
{
  "version" : "2017-02-28",
  "operation": "Invoke",
  "payload": $util.toJson($context.args)
}
```
### レスポンスマッピングテンプレート
同じくサンプルテンプレートから「Return lambda result」を選択します。
こちらも、以下の内容が自動で設定されます。
```
$util.toJson($context.result)
```
これで、AppSync側の設定は完了です！

## TypeScriptでの実装
今回フロントエンドはTypeScriptで実装していましたので、AppSyncとの接続方法も残しておきます。
一部気になる設計もあるかと思いますが、とりあえず動くことがメインの記述となります。

**開発環境**
- Node.js：v20.14.0
- TypeScript：v5.5.3

```typescript:Phone.tsx
  // Apollo ClientがGraphQLリクエストを送信するためのAppSyncエンドポイントを設定
  const httpLink = new HttpLink({
    uri: "https://xxx",
  });
  // AppSyncエンドポイントへのアクセスを認証するためのAPIキーを設定
  const authLink = setContext((_, { headers }) => {
    return {
      headers: {
        ...headers,
        "x-api-key": "yyy",
      },
    };
  });

  // Apollo Client（状態管理ライブラリ）が使用するリンクを作成
  const link = ApolloLink.from([authLink, httpLink]);
  // ApolloClientのインスタンスを作成
  const client = new ApolloClient({
    link: link,
    cache: new InMemoryCache(),
  });

  // GraphQLミューテーションを定義
  const SEND_DATA_MUTATION = gql`
    mutation SendData($input: SendDataInput!) {
      sendData(input: $input) {
        audioUrl
      }
    }
  `;
  // インターフェースを定義
  interface SendDataOutput {
    audioUrl: string;
  }
  interface SendDataInput {
    sessionId: string;
    message: string;
  }

  // Lambdaに送信する関数を定義
  async function sendDataToLambda(data: SendDataInput) {
    const response = await client.mutate<{ sendData: SendDataOutput }>({
      mutation: SEND_DATA_MUTATION,
      variables: { input: data },
    });
    // 音声ファイルのURLを取得
    const audioUrl = response.data?.sendData.audioUrl;
    // その他処理
  }

  // 使用例
  const iconClick = () => {
    const data = {
      sessionId: "sessionId",
      message: "message",
    };
    sendDataToLambda(data);
  };
```

## あとがき
私は、普段の業務でAWSやTypeScriptをほとんど使わない初心者でしたが、そこまで複雑な設計ではなかったので、使いながら慣れることができました。
本機能だけでなく、アプリケーション内の別機能でもAppSyncを利用しましたが、異なるDBやLambda関数などを1つのGraphQLスキーマで扱うので単一のエンドポイントからデータを取得できて、開発の負担はかなり減ったと思います。