🦣

AppSyncのFunctionをCDK+TypeScriptで作成する

2023/04/13に公開1

はじめに

AppSyncは、AWSでGraphQLでAPIを構築する場合に、簡単に立ち上げられるので非常に便利なサービスです。しかしながら、AppSyncのResolver、及びFunctionは、これまでVTLで記述する必要があり、VTLのコーディングを苦痛と感じることが多かったです。

しかしながら、ここ最近、

  • AppSyncのResolver、Functionが、JavaScriptをサポート
  • AWS CDKが、上記をサポート

となり、開発環境も整備されてきました。

私も、TypeScriptでコーディングしたCDKのソース群に、JavaScriptで定義した、AppSyncのFunctionのコードを定義し、

  const createCommentFn = new AppsyncFunction(this, "createCommentFn", {
      name: "createCommentFn",
      api: api,
      dataSource: dataSource.dynamoTable,
      runtime: FunctionRuntime.JS_1_0_0,
      code: Code.fromAsset(path.join(__dirname, "createComment.js")),
    })

のような定義をCDKに記載して、JavaScriptで記述したFunctionを作成していました。ただ、この方法だと、

  • TypeScriptで記述した、CDKのソース群の中に、JavaScriptのコードが混在してしまう
  • JavaScriptであるため、型付けされておらず、ケアレスミスなどが発生しやすい
  • 共通の処理を、モジュール化できない

などの課題がありました。

AppSyncのFunctionをTypeScriptで記述する

Gitの以下のissueをウォッチしていると、AppSyncのJavaScript Resolver(Function)での開発に関して、活発な意見交換がされており、

その中で、 @aws-appsync/utils がGenerics をサポートし、それにともない、CDKで簡単に、TypeScriptでJavaScript Resolver(Function)を記述する記事が上がっていました。この記事を参考にすることで、

  • schema.graphqlファイルから、GraphQLの型を自動的に生成
  • 自動生成された型を使用して、TypeScriptでJavaScript Resolver(Function)を定義
  • CDKのデプロイ時に自動的に、esbuildを実行してJavaScript化して、デプロイ

を実現することができ、すべてTypeScriptでの開発を実現できます。

元記事では、Resolver単位で作成していましたが、私の場合は、Function単位で、実施したかったので、下記のような カスタムCDK Constructを作成しました。

import { buildSync } from "esbuild"
import {
  AppsyncFunction,
  BaseDataSource,
  Code,
  FunctionRuntime,
  GraphqlApi,
} from "aws-cdk-lib/aws-appsync"
import { Construct } from "constructs"

export interface JsAppSyncFunctionProps {
  api: GraphqlApi
  dataSource: BaseDataSource
  name: string
  source: string
}

export class JsAppSyncFunction extends Construct {
  public readonly function: AppsyncFunction

  constructor(scope: Construct, id: string, props: JsAppSyncFunctionProps) {
    super(scope, id)

    // Use esbuild to transpile/bundle the resolver code
    const buildResult = buildSync({
      entryPoints: [props.source],
      bundle: true,
      write: false,
      external: ["@aws-appsync/utils"],
      format: "esm",
      target: "esnext",
    })

    if (buildResult.errors.length > 0) {
      throw new Error(`Failed to build ${props.source}: ${buildResult.errors[0].text}`)
    }
    if (buildResult.outputFiles.length === 0) {
      throw new Error(`Failed to build ${props.source}: no output files`)
    }

    // Create AppSync function from bundled code
    this.function = new AppsyncFunction(this, props.name, {
      api: props.api,
      dataSource: props.dataSource,
      name: props.name,
      code: Code.fromInline(buildResult.outputFiles[0].text),
      runtime: FunctionRuntime.JS_1_0_0,
    })
  }
}

使い方は、以下のように、JavaScriptでAppsyncFunctionを作成していた箇所を、少し修正するだけです。

  const createCommentFn = new JsAppsyncFunction(this, "createCommentFn", {
      name: "createCommentFn",
      api: api,
      dataSource: dataSource.dynamoTable,
      source: path.join(__dirname, "createComment.ts"),
    }).function

これで、TypeScriptオンリーで、バックエンドサービスの開発が可能になり、フロントエンド、バックエンド、インフラすべてTypeScriptでの開発が可能になります。

なお、TypeScriptでのFunctionの書き方などは、元記事を参考にしてください。

参考記事

Discussion

ashizakiashizaki

AppSyncの開発ツールのGraphBoltのBlogでも同じようなアプローチをしている記事がありました。こちらの記事にあるように、graphql-codegenと組み合わせて、あらかじめGraphQLのスキーマからAPIの型定義を自動生成して、それをAppSyncの関数のコーディングで利用することで、APIの定義と、その処理の実装の型の整合性を確認することができるので、非常に便利です。

https://blog.graphbolt.dev/improving-developer-experience-with-typescript-how-to-write-strongly-typed-appsync-resolvers