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
- In order to know the actual error(not just
Internal Server Error
), It returnsException#to_json
to a client.require 'json/add/exception'
enablesException#to_json
. Don't use this in production as it disclose internal errors. attribute_updates
is not recommended. You should useUpdateExpression
instead.- All path parameters coming from client → APIGW is in
event['pathParameters']['proxy']
. As the parameter can benil
, usedig
method to getnil
(not an exception) for the case that the hash key does not exist.
That's it and it worked!