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