10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AWS AmplifyでTypeScriptとExpressを使ったLambda関数のRESTful APIを作る方法

Posted at

はいさい!ちゅらデータぬオースティンやいびーん!

概要

AWS AmplifyのCLIを通して、API GatewayとLambdaを用いたRESTful APIを作る際に、サーバーレスのExpressと、TypeScriptを使う方法を紹介します。

背景

まず、サーバーレスのExpressをなぜLambdaで使うのかですが、単純に筆者は、Expressに慣れており、Expressならさっさと書けるからです。筆者と同様な作者はいるかと思いますが、いかがでしょうか?

その他に、Expressで書くと、Lambdaを脱出する必要があったら、コードを再利用することもできるので、おすすめです。

実際、Expressをサーバーレスで実行するために、オープンソースのプロジェクトがあり、現役で開発が進んでいます。それほど人気な書き方・実装方法だと筆者は捉えています。

そして、なぜTypeScriptをLambdaに導入するかですが、こちらは、コードの品質を上げるためです。型がないバックエンド開発を筆者はしたくないのです。JavaScriptだけだと、バックエンド開発においてはどうしても物足りなさを感じます。

ただ、Amplify CDKを使っているプロジェクトにおいて、どうやってTypeScriptを導入できるかが明確でないので、正式ドキュメントも混ぜて解説していきたいと思いました。

Amplifyプロジェクトをお持ちでない方は、前回の記事でまずAmplifyのプロジェク開設をしてから本記事をお読みになることをお勧めします。

RESTFul APIをAmplifyプロジェクトに追加する

まずは、Amplify CLIを使って、RESTFul APIを追加します。Amplify CLIで追加すると、テンプレートのコードを生成してくれるので、出発しやすくなります。

以下のコマンドを実行して、追加の設定を始めます。

amplify add api

最初のダイアログでRESTを選択します。
スクリーンショット 2022-08-01 15.59.28.png
APIのリソース名を付け、APIのパスも設定します。今回は便宜的に/todosにしますが、任意です。
スクリーンショット 2022-08-01 17.07.21.png
次はLambda関数の関数名を設定します。
スクリーンショット 2022-08-01 17.08.32.png
そして、Runtimeの選択では、NodeJSを選択し、templateでは、Serverless ExpressJSを選択します。
スクリーンショット 2022-08-01 17.09.22.png
詳細設定はnで大丈夫なのですが、仮にyを入力すると、Lambda関数のLayers、Secret Managerのアクセス、環境変数など、様々な設定ができます。

ちなみに、これらの設定は後からもできるのでこの段階でやらなくてもいいです。
スクリーンショット 2022-08-01 17.11.32.png

次、APIへのアクセスに認証を必要とするかの質問ですが、こちらはCognitoのUser Poolと併用している場合、Cognitoで認証を経ているユーザーのみAPIへアクセスできるように制限することができます。

今は制限しませんが、公開されるので、ご注意ください。
スクリーンショット 2022-08-01 17.13.51.png
他のパスも追加するか聞かれますが、不要なので、n
スクリーンショット 2022-08-01 17.16.07.png
すると、ローカルの設定にAPIの設定が入りましたが、AmplifyのAWSクラウドに反映されていないので、以下のコマンドを実行して反映させます。

amplify push

実行すると、今回加える変更(API GatewayとLambda関数の追加)が表示されます。yで実行してみましょう。
スクリーンショット 2022-08-01 17.17.45.png
成功すると、API Gatewayのエンドポイントが出力されます。
スクリーンショット 2022-08-01 17.22.10.png
アクセスしてみるとLambda Expressのテンプレートのままのレスポンスが来ます。

ターミナルに出力されたエンドポイントに/todosを足さないと、missing tokenというエラーが返ってくるのでご注意!
スクリーンショット 2022-08-01 17.23.09.png
これでAPIの開設は終わり、TypeScriptを導入する準備ができました。

ExpressのLambda関数にTypeScriptを追加する

これからは、上記のAPI追加で生成されたLambda関数にTypeScriptを導入していきます。ここからの説明は、Amplify正式ドキュメントを参考にしています。

Lambda関数用のpackage.jsontsconfig.jsonを作成する

まずは、TypeScriptをコンパイルするためのパッケージをインストールする必要があるので、先ほど生成されたLambda関数のダイレクトリにcdで入りましょう。

新しいLambda関数は./amplify/backend/functionの中に入っています。筆者の場合は以下のようなコマンドを実行しました。

cd amplify/backend/function/AustinTSExpressLambdaFunc/

cdができたら、package.jsonを作ります。

yarn init

そして、必要なパッケージをインストールします。

yarn add -D typescript @types/node @types/express @types/aws-serverless-express esbuild express body-parser aws-sdk @vendia/serverless-express

tsconfig.jsonも同じダイレクトリに作成します。

touch tsconfig.json

中身は以下の推奨設定でいいかと思います。AWSのLambdaでTypeScriptを使用する方法について解説する正式ドキュメントから拝借させていただいています!

baseUrlおよびincludeの設定では/libというフォルダーを指していますが、これから作ります。ここにTypeScriptのファイルを入れていきます。

amplify/backend/function/AustinTSExpressLambdaFunc/tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "strict": true,
    "preserveConstEnums": true,
    "noEmit": false,
    "sourceMap": false,
    "module":"commonjs",
    "moduleResolution":"node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "outDir": "./src",
    "allowJs": true,
    "lib": ["dom", "es6"],
    "baseUrl": "./lib",
  },
  "exclude": ["node_modules"],
  "include": ["./lib/**/*.ts"],
}

/libに既存のExpressの.jsファイルを移動し、.tsに変換する

次に行うのは、テンプレートで生成されたExpressの.jsファイルを/lib/*.tsに移動しつつ、拡張子を変えることです。

mkdir lib
mv src/*.js lib/
cd lib
for f in *.js; do mv -- "$f" "${f%.js}.ts"; done
cd ..

二つのファイルのために上記のようなコマンドはOver killだと思いますが...それはさておき、これでは必要なファイルをsrcから抽出できたので、残っているファイルは削除しましょう。

srcのフォルダー自体は必要なので、再度作っておきます。

rm -rf src/
mkdir src

ExpressのテンプレートのTypeScriptエラーを解消する

これからは、テンプレートのファイルをTypeScriptらしい書き方に直すのと、エラーを解消していきます。
元々あったapp.jsからコメントを削除すればこうなります。

amplify/backend/function/AustinTSExpressLambdaFunc/lib/app.ts
const express = require('express')
const bodyParser = require('body-parser')
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')

const app = express()
app.use(bodyParser.json())
app.use(awsServerlessExpressMiddleware.eventContext())

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*")
  res.header("Access-Control-Allow-Headers", "*")
  next()
});


app.get('/todos', function(req, res) {
  res.json({success: 'get call succeed!', url: req.url});
});

app.get('/todos/*', function(req, res) {
  res.json({success: 'get call succeed!', url: req.url});
});

app.post('/todos', function(req, res) {
  // Add your code here
  res.json({success: 'post call succeed!', url: req.url, body: req.body})
});

app.post('/todos/*', function(req, res) {
  // Add your code here
  res.json({success: 'post call succeed!', url: req.url, body: req.body})
});


app.put('/todos', function(req, res) {
  // Add your code here
  res.json({success: 'put call succeed!', url: req.url, body: req.body})
});

app.put('/todos/*', function(req, res) {
  // Add your code here
  res.json({success: 'put call succeed!', url: req.url, body: req.body})
});


app.delete('/todos', function(req, res) {
  // Add your code here
  res.json({success: 'delete call succeed!', url: req.url});
});

app.delete('/todos/*', function(req, res) {
  // Add your code here
  res.json({success: 'delete call succeed!', url: req.url});
});

app.listen(3000, function() {
    console.log("App started")
});

module.exports = app

これを以下のように修正します。

amplify/backend/function/AustinTSExpressLambdaFunc/lib/app.ts
import express from "express";
import bodyParser from "body-parser";

const app = express();
app.use(bodyParser.json());

app.use(function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "*");
  next();
});

app.get("/todos", function (req, res) {
  res.json({ success: "get call succeed!", url: req.url });
});

app.get("/todos/*", function (req, res) {
  res.json({ success: "get call succeed!", url: req.url });
});

app.post("/todos", function (req, res) {
  res.json({ success: "post call succeed!", url: req.url, body: req.body });
});

app.post("/todos/*", function (req, res) {
  res.json({ success: "post call succeed!", url: req.url, body: req.body });
});

app.put("/todos", function (req, res) {
  res.json({ success: "put call succeed!", url: req.url, body: req.body });
});

app.put("/todos/*", function (req, res) {
  res.json({ success: "put call succeed!", url: req.url, body: req.body });
});

app.delete("/todos", function (req, res) {
  res.json({ success: "delete call succeed!", url: req.url });
});

app.delete("/todos/*", function (req, res) {
  res.json({ success: "delete call succeed!", url: req.url });
});

app.listen(3000, function () {
  console.log("App started");
});

export default app;

index.jsで以下のようになっていますが、こちらも修正します。

amplify/backend/function/AustinTSExpressLambdaFunc/lib/index.ts
const awsServerlessExpress = require('aws-serverless-express');
const app = require('./app');

/**
 * @type {import('http').Server}
 */
const server = awsServerlessExpress.createServer(app);

/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */
exports.handler = (event, context) => {
  console.log(`EVENT: ${JSON.stringify(event)}`);
  return awsServerlessExpress.proxy(server, event, context, 'PROMISE').promise;
};

aws-serverless-expressではなく、元々の@vendia/serverless-expressを使うようにしていますが、これはAWSに依存するものを減らすためです。

amplify/backend/function/AustinTSExpressLambdaFunc/lib/index.ts
import serverlessExpress from "@vendia/serverless-express";
import app from "./app";

exports.handler = serverlessExpress({ app });

これで全てのエラーは解消されているので、ビルド手順に入ります!

esbuildでLambda関数のTypeScriptコードをコンパイルする

ここまでくると、あとはビルドするのみ!

AWS正式ドキュメントでは、esbuildの使用が推奨されているので、こちらでもそうします。

先ほど、新しく作成したpackage.jsonscriptsに以下のコマンドを追加します。

amplify/backend/function/AustinTSExpressLambdaFunc/package.json
{
  "name": "AustinTSExpressLambdaFunc",
  "version": "1.0.0",
  "main": "src/index.js",
  "license": "MIT",
  "devDependencies": {
    "@types/aws-serverless-express": "^3.3.5",
    "@types/express": "^4.17.13",
    "@types/node": "^18.6.3",
    "@vendia/serverless-express": "^4.10.1",
    "aws-sdk": "^2.1185.0",
    "body-parser": "^1.20.0",
    "esbuild": "^0.14.51",
    "express": "^4.18.1",
    "typescript": "^4.7.4"
  },
  "scripts": {
    "build": "esbuild ./lib/index.ts --bundle --minify --external:aws-sdk --sourcemap --platform=node --target=es2020 --outfile=./src/index.js"
  }
}

ここで重要なのは、package.jsonではaws-sdkのパッケージをインストールしていますが、これはTypeScriptのインタプリターのためのみで、バンドルに含めたくないことです。
それゆえに、esbuildのコマンドで--external:aws-sdkと、バンドルから排除しています。

なぜかというと、aws-sdkはLambda関数のNodeJS環境なら、インストールせずに使うことができます。また、含めると、ものすごく大きくなり、重くなります

デプロイできるようにする

最後に、amplify pushのコマンドを実行した時に、自動的にビルドしてくれるようにします。

ルート・ダイレクトリーのpackage.jsonscriptを追加する

Amplifyプロジェクトのルートダイレクトリにある./package.jsonに特別なscriptコマンドを追加する必要があります。

ありがたいことに、正式ドキュメントにそのやり方が書いてあります。

しかし、注意していただきたいことがあります。複数のpackage.jsonがあります。これから追加するscriptは、Lambda関数のamplify/backend/function/AustinTSExpressLambdaFunc/package.jsonではなく、ルートダイレクトリーにある主要な./package.jsonに加えなければなりません

筆者は、これを理解できずに数時間を無駄にしながらも懸命にパソコンとAmazonを憎み続けました。

package.json
{
  "name": "amplify-qiita",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack --config webpack.prod.config.js",
    "start": "webpack serve",
    "amplify:AustinTSExpressLambdaFunc": "cd amplify/backend/function/AustinTSExpressLambdaFunc/ && yarn build"
  },
  "devDependencies": {
    "@babel/core": "^7.18.9",
    "@babel/preset-env": "^7.18.9",
    "babel-loader": "^8.2.5",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.6.1",
    "ts-loader": "^9.3.1",
    "typescript": "^4.7.4",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.9.3",
    "webpack-manifest-plugin": "^5.0.0"
  }
}

amplify:<Lambda関数名>という名前のscriptを追加し、先ほど追加したbuildのコマンドを実行するようにします。

Amplifyのバックエンドをプッシュしてみる

Lambda関数のソースコードを自動的にコンパイルしてくれる設定ができたので、うまくデプロイできるか試してみましょう。

amplify push

Lambda関数のFunctionの変更を検知してくれているので、yを入力してプッシュします。
スクリーンショット 2022-08-02 9.00.43.png

問題発生!M1 Macbookをご利用の皆様ご注意を。

筆者はApple SiliconのARMチップで開発していますが、上記のコマンドを実行すると以下のようなエラーが出ます。
スクリーンショット 2022-08-02 9.03.55.png
原因は、Amplify CLIにあります。

筆者の場合は、Amplify CLIがIntelのプロセスでRosettaに翻訳されながら実行されており、darwin-64の環境になっているのに対して、ローカルで実行するyarnは、M1ネイティブでdarwin-arm64の環境になっているのです。

これでバグるわけです。

対応策は以下のように、package.jsonscriptにインストール済みのパッケージを削除して再インストールするコマンドを追加することです。

package.json
{
  "name": "amplify-qiita",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack --config webpack.prod.config.js",
    "start": "webpack serve",
    "amplify:AustinTSExpressLambdaFunc": "cd amplify/backend/function/AustinTSExpressLambdaFunc/ && rm -rf node_modules && yarn install && yarn build"
  },
  "devDependencies": {
    "@babel/core": "^7.18.9",
    "@babel/preset-env": "^7.18.9",
    "babel-loader": "^8.2.5",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.6.1",
    "ts-loader": "^9.3.1",
    "typescript": "^4.7.4",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.9.3",
    "webpack-manifest-plugin": "^5.0.0"
  }
}

上記の変更を加えた上でもう一度amplify pushを実行してみます。
スクリーンショット 2022-08-02 9.17.42.png
無事に成功しました!よかったさ

M1、好きだけど、時々困りますねぇ。:sweat_smile:

エンドポイントにアクセスしてみる

デプロイできたわけですので、エンドポイントにもう一度アクセスしてみましょう。問題なくデプロイできていれば、依然と全く同じレスポンスが帰ってくるはずです。
スクリーンショット 2022-08-02 9.20.18.png
おお!TypeScript、使えるぞ!かりゆし:star:

Lambda関数のコンソールも確認してみると、esbuildらしいコードがindex.jsに入っています。
スクリーンショット 2022-08-02 9.25.10.png

まとめ

これまで、Amplify CLIでRESTful APIを作り、Expressを使ったLambda関数のソースコードをTypeScriptに変換して、それをamplify pushでコンパイルできるようにしてきましたが、いかがでしょうか?

やはり、TypeScriptは筆者の幸せの源ですので、どこでもいつでも便利に使えるようにしたいのですが、Amplifyのバックエンドでも使えるのはこの上なく喜ばしいことです。

筆者と同じくTypeScript愛好家もいらっしゃると思いますので、どんどん色んなプロジェクトに入れていきましょう!

10
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?