AWS AmplifyでTODOアプリのバックエンドを作る

5 min read読了の目安(約4500字

はじめに

こんな環境で動くLambdaのRubyコードを書いたという話です。バックエンドサービスの構築自体は前回、Amplifyでざっとつくっていて、LambdaのランタイムをRubyに置き換えています[1]

            ┌──────────┐      ┌──────────┐     ┌──────────┐
            │          │      │  Lambda  │     │          │
Client───────►  APIGW  ├──────►   with   ├─────► DynamoDB │
            │  (REST)  │      │  RubySDK │     │          │
            └──────────┘      └──────────┘     └──────────┘

API

TODOアプリなので、APIGWのREST APIの定義はこんな感じにしようと思います

GET /tasks         List all tasks
GET /task/1        Get a task by id
POST /tasks        Create new task
PUT /tasks         Update a task
DELETE /tasks/1    Delete a task by id

APIGWの設定自体はAmplifyがやってくれているのですが、コンソールで見るとこんな設定になっています。

 /                        
 |_ /todos        Main resource. Eg: /todos
    ANY           Methods: DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT
    OPTIONS       Allow pre-flight requests in CORS by browser
    |_ /{proxy+}  Eg: /todos/, /todos/id
       ANY        Methods: DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT
       OPTIONS    Allow pre-flight requests in CORS by browser

ANYで全てのHTTPメソッドをマッチさせて、Lambda proxy integrationsでリクエストをそのままLambdaに流してしまえってやつです。また{proxy+}をつかって、URLの全てのパスパラメータ(/tasks/100など)をマッチさせてこれもLambda側に渡します

クライアントが送るデータは、こういう感じのものを想定

payload
{
    "task-id": "30",
    "is-active": true,
    "task-name": "Buy some coffee",
    "updated-at": 1616047389,
    "created-at": 1616047389,
    "user-id": "110"
}

DynamoDB

Amplifyでtask-idをキーとしてテーブルtodoTable-devを作成している[1:1]

Lambda

APIGWの設定を踏まえて、Lambda側のコードはこんな感じ。Rubyは2.7

lambda_handler.rb
require 'json'
require 'json/add/exception' #...❶
require 'aws-sdk-dynamodb'

def add_task(table, body)
  begin
    table.put_item({ item: body })  
    list_task(table)
  rescue => e
    { statusCode: 500, body: e.to_json } #...❶
  end
end

def delete_task(table, task_id)
  begin
    params = { table_name: table, key: { 'task-id': task_id } }
    table.delete_item(params)
    list_task(table)
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end

def update_task(table, body)
  begin
    params = {
      table_name: table,
      key: { 'task-id': body['task-id'] },
      attribute_updates: { #...❷
        'is-active': { value: body['is-active'], action: "PUT" },
        'task-name': { value: body['task-name'], action: "PUT" },
        'updated-at': { value: body['updated-at'], action: "PUT" }
      }
    }
    table.update_item(params)  
    list_task(table)
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end

def list_task(table)
  begin
    scan_output = table.scan({ limit: 50, select: "ALL_ATTRIBUTES" })
    { statusCode: 200, body: JSON.generate(scan_output['items']) }
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end

def get_task(table, task_id)
  begin
    params = { key: { 'task-id': task_id } }
    task = table.get_item(params)
    { statusCode: 200, body: JSON.generate(task['item']) }
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end

def lambda_handler(event:, context:)
  begin
    http_method = event['httpMethod']
    dynamodb = Aws::DynamoDB::Resource.new(region: 'us-east-2')
    table = dynamodb.table('todoTable-dev')
    
    case http_method
      when 'GET'
        path_param = event.dig('pathParameters', 'proxy') #...❸
        if path_param.nil?
          list_task(table)
        else
          get_task(table, path_param) 
        end
      when 'PUT'    then update_task(table, JSON.parse(event['body']))
      when 'POST'   then result = add_task(table, JSON.parse(event['body']))
      when 'DELETE' then delete_task(table, event['pathParameters']['proxy']) #...❸
      else 0
    end
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end
  1. 例外が発生した時にInternal Server Errorじゃなくて、エラーの中身をException#to_jsonとしてクライアントに返すようにしている。Exception#to_jsonを使うためには、require 'json/add/exception'が必要。プロダクションではやったらあかんよ
  2. DynamoDBの値更新ではattribute_updatesはすでに非推奨らしい。UpdateExpressionが推奨されているらしいが、こっちのほうが記述がめんどくさいと思う
  3. クライアント→APIGWからくるPath Parameterはevent['pathParameters']['proxy']に入っている。nilがあり得るので、digを使うことで、キーが存在しない場合にエラーとせず、nilを返すようにしている

こんな感じでとりあえず動きました!Webアプリ作成のハードルもどんどん下がってきてますね

参考にしたサイト

https://gerard-sans.medium.com/create-a-rest-api-integrated-with-amazon-dynamodb-using-aws-amplify-and-vue-5be746e43c22

https://zenn.dev/masaino/articles/8f9b6aaf9ed8bb
脚注
  1. 詳しくはこちら ↩︎ ↩︎