Create a Todo app backend with Amplify and Ruby

·

3 min read

Introduction

This is the story of how I wrote the Ruby code for Lambda to run in the following environment. The backend service itself was roughly built with Amplify last time, and the Lambda runtime was replaced with Ruby's.

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

API

The REST API for the Todo app is like this:

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

The APIGW setting is done by Amplify and the configuration in the console is:

 /                        
 |_ /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

The configuration captures all HTTP methods with ANY and the Lambda proxy integrations forwards everything(parameters) to the backend Lambda. {proxy+} matches all URL path parameters(eg. /tasks/100) and pass them to the backend.

Payload which a client sends is:

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

DynamoDB

I only created the todoTable-dev table with the primary key task-id with Amplify

Lambda

For the APIGW setting above, the corresponding Lambda code is as follows. The ruby version is 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. In order to know the actual error(not just Internal Server Error), It returns Exception#to_json to a client. require 'json/add/exception' enables Exception#to_json. Don't use this in production as it disclose internal errors.
  2. attribute_updates is not recommended. You should use UpdateExpression instead.
  3. All path parameters coming from client → APIGW is in event['pathParameters']['proxy']. As the parameter can be nil, use dig method to get nil(not an exception) for the case that the hash key does not exist.

That's it and it worked!

Reference