Serverless FrameworkでREST APIを作成してみた (Lambda, API Gateway, DynamoDB, Node.js)

#はじめに
前回の記事でServerless FrameworkでLambdaの開発環境を構築しました。

https://qiita.com/suzuki0430/items/1d8aa2d9900fd867db9c

引き続きServerless Frameworkの勉強ということで、Lambda(Node.js), API Gateway, DynamoDBの構成でREST APIを作成してみることにしました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/3989b6d2-8d78-846e-3bdf-36475b111178.png" width="800">

#実装
Lambda関数の中身をjsファイル、構築するリソースの設定を`serverless.yml`に記述します。

##Lambda関数の作成
REST APIを実装するにあたり、データ作成(Create), 全データ取得(List), データ取得(Get), データ更新(Update), データ削除(Delete)の機能が必要です。
そのため、各機能ごとにLambda関数を作成する必要があります。

それぞれの関数を説明すると長くなるので、データ更新用の関数(`create.js`)のみ説明します。

```js:create.js
'use strict';

const uuid = require('uuid');
const AWS = require('aws-sdk');

const dynamoDb = new AWS.DynamoDB.DocumentClient(); //ドキュメントクライアントを使用してNode.jsでDynamoDBにアクセス

module.exports.create = (event, context, callback) => {
  const timestamp = new Date().getTime();
  const data = JSON.parse(event.body); //JSONをJSのオブジェクトに変換

  //textがstringじゃないときにエラーを返す(バリデーション)
  if (typeof data.text !== 'string') {
    console.error('Validation Failed');
    callback(null, {
      statusCode: 400,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Couldn\'t create the todo item.',
    });
    return;
  }

  //テーブル名(TableName)と各カラム(Item)のオブジェクト
  const params = { 
    TableName: process.env.DYNAMODB_TABLE,
    Item: {
      id: uuid.v1(),
      text: data.text,
      checked: false,
      createdAt: timestamp,
      updatedAt: timestamp,
    },
  };

  //DynamoDBへのデータ保存
  dynamoDb.put(params, (error) => {
    //エラー処理
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t create the todo item.',
      });
      return;
    }

    //処理成功時のレスポンス
    const response = {
      statusCode: 200,
      body: JSON.stringify(params.Item), //JSオブジェクトをJSONに変換
    };
    callback(null, response);
  });
};

```

Node.jsからDynamoDBにアクセスするために、ドキュメントクライアントを利用します。

`const dynamoDb = new AWS.DynamoDB.DocumentClient();`と定義することで、`dynamoDb.get`(データ取得)、`dynamoDb.put`(データ保存)のようにJSでDynamoDBのデータの基本操作を行うことができます。

データ保存を行うためには、テーブル名(TableName)とカラム(Item)のオブジェクト(params)を用意して`dynamoDb.put(params, (error)`のように引数で渡してあげればOKです。

```javascript
  const params = { 
    TableName: process.env.DYNAMODB_TABLE,
    Item: {
      id: uuid.v1(),
      text: data.text,
      checked: false,
      createdAt: timestamp,
      updatedAt: timestamp,
    },
  };
```

`dynamoDb.put(params, (error)`の中身には、データ保存の処理が失敗したときと成功したときのcallbackを記述します。
処理が成功していれば、ステータスコード(`statusCode`)と保存内容(`body`)を返します。

```javascript
    //処理成功時のレスポンス
    const response = {
      statusCode: 200,
      body: JSON.stringify(params.Item), //JSオブジェクトをJSONに変換
    };
    callback(null, response);
```

https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/dynamodb-example-document-client.html

##serverless.ymlの作成
API GatewayやDynamoDBのリソースを構築するために、serverless.ymlを記述します。

###API Gatewayの構築
公式ドキュメントを参考に、Lambda関数のトリガーイベントとしてAPIGatewayを作成します。

https://www.serverless.com/framework/docs/providers/aws/events/apigateway/#custom-response-templates

```yml
functions: #Lambdaの構築
  create: #関数名を指定
    handler: todos/create.create #handler関数の指定(ディレクトリパス.モジュール名)
    events: #Lambdaのイベントトリガーを指定
      - http: #httpイベントをトリガとしてAPI Gatewayを構築
          path: todos #リソースの指定
          method: post #メソッドの指定
          cors: true #cors有効/無効の指定
```

###DynamoDBの構築
DynamoDBの構築についても、公式ドキュメントを参考にします。

https://www.serverless.com/dynamodb

```yml
resources: #リソースの構築
  Resources:
    TodosDynamoDbTable: #DynamoDBの構築
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
```

###IAM設定
LambdaからDynamoDBにアクセスするための権限を付与するために、`Provider`に`iamRoleStatements`を追記します。


```yml
provider:
  name: aws
  runtime: nodejs12.x
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: 'arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}'
```

##デプロイ
以下のコマンドでデプロイを行います。

```
sls deploy -v
```
AWSコンソールのLambdaを開くと、作成した関数がデプロイされているのを確認できます。
![スクリーンショット 2021-05-01 8.53.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/844a1e4b-6be3-5f58-633b-48422e6d15de.png)

API Gatewayを開くと`dev-serverless-rest-api-with-dynamodb`が作成されています。
![スクリーンショット 2021-05-01 8.57.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/1e0d2eea-390d-d150-5c84-34e73a09f5fb.png)

また、DynamoDBにもテーブルが作成されます。
![スクリーンショット 2021-05-01 9.01.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/5c2d70b6-0a20-c0c7-34f9-a278eb1c8e79.png)

##動作確認
insomniaを使って挙動をみてみます。

https://insomnia.rest/

エンドポイントはデプロイの際にターミナルに表示されたものを使います。

```
endpoints:
  POST - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos
  GET - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos
  GET - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
  PUT - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
  DELETE - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
```

試しにPOSTリクエスト(Create)を飛ばしてみると、`200 OK`とレスポンスが返ってきます。
![スクリーンショット 2021-05-01 9.34.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ca48cf0a-afb7-4de1-0ede-ffbcd299d401.png)

また、DynamoDBのテーブルにはデータが追加されています。
![スクリーンショット 2021-05-01 9.38.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ed2d66e8-44a9-dd07-6163-369460bbffb4.png)

#おわりに
Serverless Framework超便利だね。

#参考資料

https://qiita.com/t_okkan/items/546330b5f4da720c71a7