技術

TypeScript×AWS Lambda×API GatewayでREST API構築

AWS LambdaとTypeScriptでREST APIを作ってみました。

LambdaでTypeScriptを選択することはあまりないと思うのですが、やってみると以外と良かったので、チュートリアル的なものも含めてまとめてみたいと思います。

LambdaとTypeScriptの組み合わせになった経緯

私は今までREST APIを作るとき、自分でアーキテクチャーを決めていいときは、LambdaとPythonで作成していました。

今回のプロジェクトもほぼ初期フェーズからの参加で割と自由にやっていいとのことでした。ただ一つ、フロントとなるべく技術を合わせて欲しいという要望があり、JavaScript、もしくはTypeScriptでやって欲しいとのことでした。

インフラのちょっとしたスクリプトならともかく、REST APIのようなバックエンドのサービスを生JSでやりたくないなと思ったので、TypeScriptでやってみようと思いました。

Lambdaは、TypeScriptを直接サポートしているわけではありませんが、Node.jsが動く以上、トランスパイルすればいいのでTyepScritpで書くことができます。

プロジェクトのセットアップ

導入に際しては、以下のブログが大変参考になりました。
https://www.wantedly.com/companies/linc-well/post_articles/354563

フレームワークは、Serverless FrameworkとServerless Frameworkの公式テンプレートの1つのaws-nodejs-typescriptを使います。

まずは環境からセットアップします。2022年2月現在、LambdaでサポートされているNode.jsの最新バージョンは14系なので、Node.js14とServerless Frameworkをインストールしておきます。

インストールコマンドは略、以下はインストール後の確認コマンドです。

$ node -v
v14.18.2

$ sls --version
Framework Core: 2.69.0
Plugin: 5.5.1
SDK: 4.3.0
Components: 3.18.1

テンプレートを利用したプロジェクトを作成します。

$ sls create --template aws-nodejs-typescript

カレントディレクトリに以下のファイルとフォルダが作成されます。

$ ls -1
README.md
package.json
serverless.ts
src/
tsconfig.json
tsconfig.paths.json

srcフォルダの中身は、

functions/
libs/

となっています。functions/にhandlerがあって、libsにfunctionsにある各functionで読み込まれる共通処理が書かれています。これを

$ sls deploy

とすると、API Gatewayが生成されて、トランスパイルされたJSファイルがLambdaにdeployされます。

serverlessコンフィグファイルがyamlでなくtsになっている

aws-nodejs-typescriptからプロジェクトを作成した場合、serverlessのコンフィグファイルがyamlでなくtsファイルになっています。

最初は戸惑いましたが、やっていることはyamlと同じなので特に問題ないかと思います。慣れれば、tsファイルのほうがimportしやすかったり型参照できたりと便利です。

従来のserverless.ymlでは、functions: {}に直接handlerやeventを記述することが多かったと思います。aws-nodejs-typescriptでは、src/functions/のindex.tsに記述して、serverless.tsではそれをimportして記述しています。

exportされているfunctionのhandler pathは以下のように記述されています。

handler: `${handlerPath(__dirname)}/xxx.xxx`,

このhandlerPath(__dirname)は参照しても何をしているのかパッと見わかりづらいのですが、index.tsがあるpathを取ってきています。なのでhandlerファイルとindex.tsが同じディレクトリにあれば、あとはfunctions内の構成は自由に変えられます。以下の様にAPIの種類ごとにフォルダを分けることができます。

src/functions/
    |-api_cat1/
    |-index.ts
    |-xxx.ts
    |-yyy.ts
    |-api_cat2/
    |-index.ts
    |-xxx.ts
    |-yyy.ts

ローカルからの実行

作成した関数は、以下のコマンドでローカルで実行することができます。

$ sls invoke local -f {関数名} --path {mock.jsonのpath}

関数名は、index.tsでexportしている関数名です。mock.jsonのパスは、関数に渡すパラメーターのJSONです。

aws-nodejs-typescriptのhelloサンプルでは以下のようになっています。

{
"headers": {
  "Content-Type": "application/json"
},
"body": "{\"name\": \"Frederic\"}"
}

これは、bodyでリクエストパラメータを渡す場合です。パスパラメータやクエリパラメータを渡す場合は以下の様に書けます。

{
"pathParameters": {
"xxx": "hoge"
},
"queryStringParameters": {
"yyy": "fuga"
}
}

クリーンアーキテクチャを採用したフォルダ構成を考える

フォルダ構成はServerless Frameworkがdeploy時にいい感じでトランスパイルしてくれるの自由に配置できます。functionsやlibsすら消しても大丈夫でしょう。

私は元の構成はあまり崩したくなかったので、functionsにcontrollerを配置して、libsにusercaseやrepositoryを配置しました。

今回のプロジェクトでは以下の様なフォルダ構成にしました。

src
|-functions
|   |-controller
|   |-mock
|   |-schema
|-libs
|   |-domain
|   |   |-entity
|   |   |-usecase
|   |-helper
|   |-repository

functionsにcontrollerフォルダを生成して、その配下にhandlerファイルを作成していっています。aws-nodejs-typescriptのサンプルでは、handlerと同じ改装に、mockファイルとschemaファイルもあったのですが、今回は別フォルダに分けました。endpointの数がかなり多かったので、controllerのフォルダやファイルも多くなり、別フォルダにまとめておいた方が使いやすいと判断しました。

libs配下にはcontrollerから呼び出される処理を書いています。クリーンアーキテクチャを採用しました。usecase配下はinterfaceファイルとinteractorファイルがあり、repository配下はinterfaceファイルとImplementationファイルから成っています。

依存関係は以下の様にしています。

helperは特に依存はこだわらず、どのレイヤーからも参照していいことにしています。

今回作成した奴はendpointは多いものの、ビジネスロジックはシンプルでしたのでvalue objectは作りませんでした。またディレクトリ構成をできるだけシンプルにしたかったので、type用のディレクトリも用意しませんでした。repositoryのimplやhelperに書いたりしています。

ValidationはできるだけAPI Gatewayに任せたいけど・・・

aws-nodejs-typescriptはJSON Schemaを使ったvalidationが組み込まれています。

index.txの関数定義に

events: [
  request: {
    schemas: {
      'application/json': sampleSchema
    }
  },
]

のように記述し、schemaファイルで、

export const sampleSchema = {
  type: 'string'
} as const

のように記述すれば、API GatewayでValidationしてくれます。

JSON Schemaの記法で書けて大変便利なのですが、以下の問題点があります。

  1. RequestのBodyにしか適用できない(?)、パスパラメータやクエリパラメータには適用できない
  2. Validationに引っかかった場合、API Gatewayが返すエラーメッセージが具体的でない
  3. UnitTestがしづらい

1.のパスパラメータやクエリパラメータに適用する方法がわかりませんでした。JSON Schemaなので、JSONにしか適用できないでしょうから、仕方がないと思いますが。

あと、2.ですが、API Gatewayの返すエラーメッセージが全て

{
  "message": "Invalid request body"
}

になります。

JSON Schemaでvalidationエラー検知をした段階では、どんなruleに違反したかの具体的なエラーは出るのですが、API Gatewayを通すときに全部、上記のエラーメッセージに変換されて返しています。さすがにこれは対象方法がやり方があると思うのですが、まだ調べ切れていないです。正常系のリターンにも影響が出そうなので、あまりやりたくないって感じではあります。

3.のUnitTestがしづらいといは、Validationの一部(リクエストボディのみ・・・)をAPI Gatewayに預けている以上、ここのチェックはUnitTestでは実現できないでしょう。E2EテストのみでOKと割り切るしかないです。

今回は非公開の身内向けのAPI、スピード優先、UnitTestは特に要求されていないという条件でしたので、このままちょっと行けてないvalidationでリリースしました。

総論・雑感

以上、AWS LambdaとTypeScriptでREST APIの構築についてでした。

使ってみた感想ですが、予想以上に良かったです。使う前までは、LambdaならPythonでいいでしょと思っていたのですが、今後またアーキテクチャー選定とかあれば、この構成も候補になりそうです。

やはり型静的で書けて、コンパイルでエラー検知できるというのは嬉しいです。PythonでもTypeHintやCIにいろいろ盛り込めば似たようなことはできるのですが、安定感で言えばTypeScriptのほうが上かと思います。

またPythonは最近は最新バージョンにいい機能が入ってきているので、できるだけ最新バージョンで書きたいところなのですが、Lambdaの対応バージョンがちょっと古めというジレンマもあります。この辺も時期によっては、PythonでなくTypeScriptを考慮する要因になるかと思います。

今回の案件を通して、TypeScriptがより好きになりました!