React
フロントエンドはReactで適当にTodoアプリを作っておきました。
process.env.REACT_APP_BASE_URL
にはこの後作成するAPI Gatewayのエンドポイントが入ります。
import { useEffect, useState } from 'react';
import Todo from './components/Todo';
import AddForm from './components/AddForm';
import axios from 'axios';
const App = () => {
const [todos, setTodos] = useState([])
const getTodos = () => {
axios.get(`${process.env.REACT_APP_BASE_URL}/todo`
).then(res => {
setTodos(res.data)
}).catch(e => {
console.log(e)
})
}
useEffect(() => {
getTodos()
},[])
const updateTodos = (newTodos) => {
setTodos(newTodos)
localStorage.setItem('todos', JSON.stringify(newTodos))
}
const handleAddFormSubmit = (title) => {
if (!title) return
axios.post(`${process.env.REACT_APP_BASE_URL}/todo`, {
title
}).then(() => {
getTodos()
}).catch(e => {
console.log(e)
})
}
const handleTodoCheckboxChange = (id, isCompleted) => {
axios.patch(`${process.env.REACT_APP_BASE_URL}/todo?id=${id}`, {
isCompleted
}).then(() => {
getTodos()
}).catch(e => {
console.log(e)
})
}
const handleTodoDeleteClick = (id) => {
axios.delete(`${process.env.REACT_APP_BASE_URL}/todo?id=${id}`
).then(() => {
getTodos()
}).catch(e => {
console.log(e)
})
}
const todoItems = todos.map((todo) => {
return (
<Todo
key={todo.id}
todo={todo}
onDeleteClick={handleTodoDeleteClick}
onCheckboxChange={handleTodoCheckboxChange}
/>
)
})
return (
<div className="container">
<h1>Todoリスト</h1>
<ul>
{todoItems}
</ul>
<AddForm
onSubmit={handleAddFormSubmit}
/>
</div>
)
}
export default App;
import { useRef, useState } from "react"
const AddForm = (props) => {
const [title, setTitle] = useState('')
const inputRef = useRef(null)
const handleTextChange = (e) => {
setTitle(e.currentTarget.value)
}
const handleSubmit = (e) => {
e.preventDefault();
props.onSubmit(title)
setTitle('')
inputRef.current.focus()
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={handleTextChange}
ref={inputRef}
/>
<button>Add</button>
</form>
)
}
export default AddForm
const Todo = (props) => {
const handleDeleteClick = () => {
props.onDeleteClick(props.todo.id)
}
const handleChecboxChange = () => {
props.onCheckboxChange(props.todo.id, !props.todo.isCompleted)
}
return (
<li>
<label>
<input
type="checkbox"
checked={props.todo.isCompleted}
onChange={handleChecboxChange}
/>
<span>
{props.todo.title}
</span>
</label>
<button onClick={handleDeleteClick}>Del</button>
</li>
)
}
export default Todo
Lambda
Reactで叩くAPIから呼ばれるLambda関数を作成します。
この後作成するDynamoDBのtodoテーブルに対するCRUD処理を作成します。
require 'json'
require 'aws-sdk-dynamodb'
def client
@client ||= Aws::DynamoDB::Client.new
end
def dynamo_resource
@dynamo_resource ||= Aws::DynamoDB::Resource.new(client: client)
end
def todo_table
@todo_table ||= dynamo_resource.table('todo')
end
def scan_items
response = todo_table.scan()
rescue Aws::DynamoDB::Errors::ServiceError => e
puts("Couldn't scan for todos")
puts("\t#{e.code}: #{e.message}")
raise
else
response.items.sort_by { |item| item['id'] }
end
def lambda_handler(event:, context:)
todos = scan_items
{
statusCode: 200,
body: JSON.generate(todos),
headers: {
"Access-Control-Allow-Origin": "http://localhost:3000"
}
}
end
require 'json'
require 'aws-sdk-dynamodb'
def client
@client ||= Aws::DynamoDB::Client.new
end
def dynamo_resource
@dynamo_resource ||= Aws::DynamoDB::Resource.new(client: client)
end
def todo_table
@todo_table ||= dynamo_resource.table('todo')
end
def sequence_table
@sequence_table ||= dynamo_resource.table('sequence')
end
def add_item(id, title)
todo_table.put_item(
item: {
id: id,
title: title,
isCompleted: false
}
)
rescue Aws::DynamoDB::Errors::ServiceError => e
puts("Couldn't add todo id: #{id}, title: #{title}")
puts("\t#{e.code}: #{e.message}")
raise
end
def get_next_id
response = sequence_table.update_item(
key: { tablename: 'todo' },
update_expression: "add seq :r",
expression_attribute_values: { ":r" => 1 },
return_values: "UPDATED_NEW"
)
rescue Aws::DynamoDB::Errors::ServiceError => e
puts("Couldn't update sequence")
puts("\t#{e.code}: #{e.message}")
raise
else
response.attributes["seq"]
end
def lambda_handler(event:, context:)
next_id = get_next_id
body = JSON.parse(event["body"])
title = body["title"]
add_item(next_id, title)
{
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "http://localhost:3000"
}
}
end
require 'json'
require 'aws-sdk-dynamodb'
def client
@client ||= Aws::DynamoDB::Client.new
end
def dynamo_resource
@dynamo_resource ||= Aws::DynamoDB::Resource.new(client: client)
end
def todo_table
@todo_table ||= dynamo_resource.table('todo')
end
def update_item(id, isCompleted)
response = todo_table.update_item(
key: {"id" => id},
update_expression: "set isCompleted = :r",
expression_attribute_values: { ":r" => isCompleted }
)
rescue Aws::DynamoDB::Errors::ServiceError => e
puts("Couldn't update todos")
puts("\t#{e.code}: #{e.message}")
raise
end
def lambda_handler(event:, context:)
id = event["queryStringParameters"]["id"].to_f.to_i
body = JSON.parse(event["body"])
isCompleted = body["isCompleted"]
update_item(id, isCompleted)
{
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "http://localhost:3000"
}
}
end
require 'json'
require 'aws-sdk-dynamodb'
def client
@client ||= Aws::DynamoDB::Client.new
end
def dynamo_resource
@dynamo_resource ||= Aws::DynamoDB::Resource.new(client: client)
end
def todo_table
@todo_table ||= dynamo_resource.table('todo')
end
def delete_item(id)
todo_table.delete_item(key: {"id" => id},)
rescue Aws::DynamoDB::Errors::ServiceError => e
puts("Couldn't delete todos")
puts("\t#{e.code}: #{e.message}")
raise
end
def lambda_handler(event:, context:)
id = event["queryStringParameters"]["id"].to_f.to_i
delete_item(id)
{
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "http://localhost:3000"
}
}
end
DynamoDBは項目を追加するときに自動的にidをインクリメントして挿入することができないので
createTodo.rb
のget_next_id
メソッドで次のidを取得する処理も実装しています。
DynamoDBのsequenceテーブルのseqカラムを使って実現しています。
SAM
AWS SAMを使ってデプロイするために、API Gateway, Lambda, DynamoDBのtempleteファイルを作成します。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
GetTodo:
Type: AWS::Serverless::Function
Properties:
FunctionName: samGetTodo
Handler: getTodo.lambda_handler
Runtime: ruby3.2
Timeout: 10
CodeUri: ./todo/get
MemorySize: 128
Policies:
- AmazonDynamoDBFullAccess
Events:
GetApi:
Type: Api
Properties:
Path: /todo
Method: get
RestApiId: !Ref API
CreateTodo:
Type: AWS::Serverless::Function
Properties:
FunctionName: samCreateTodo
Handler: createTodo.lambda_handler
Runtime: ruby3.2
Timeout: 10
CodeUri: ./todo/create
MemorySize: 128
Policies:
- AmazonDynamoDBFullAccess
Events:
GetApi:
Type: Api
Properties:
Path: /todo
Method: post
RestApiId: !Ref API
UpdateTodo:
Type: AWS::Serverless::Function
Properties:
FunctionName: samUpdateTodo
Handler: updateTodo.lambda_handler
Runtime: ruby3.2
Timeout: 10
CodeUri: ./todo/update
MemorySize: 128
Policies:
- AmazonDynamoDBFullAccess
Events:
GetApi:
Type: Api
Properties:
Path: /todo
Method: patch
RestApiId: !Ref API
DeleteTodo:
Type: AWS::Serverless::Function
Properties:
FunctionName: samDeleteTodo
Handler: deleteTodo.lambda_handler
Runtime: ruby3.2
Timeout: 10
CodeUri: ./todo/delete
MemorySize: 128
Policies:
- AmazonDynamoDBFullAccess
Events:
GetApi:
Type: Api
Properties:
Path: /todo
Method: delete
RestApiId: !Ref API
API:
Type: AWS::Serverless::Api
Properties:
Name: sam-todo-api
EndpointConfiguration: REGIONAL
StageName: dev
# 不要な「Stage」ステージが作られるのを防ぐ
OpenApiVersion: 3.0.2
TodoDB:
Type: AWS::Serverless::SimpleTable
Properties:
PrimaryKey:
Name: id
Type: Number
TableName: todo
SequenceDB:
Type: AWS::Serverless::SimpleTable
Properties:
PrimaryKey:
Name: tablename
Type: String
TableName: sequence
sam deploy
でデプロイすることができます。
感想
筆者はAWS資格のSAAとDVAを取得した後にこのアプリを作成しました。
資格の勉強だけしても実際アプリを作ってたりしないとAWSを使えるようにならないと思うので
やはり手を動かしてみることは大事だと感じました。
後、SAMでAWSリソースを作成する前に一回マネジメントコンソール画面から手動でリソースを作成するなどするとより理解が深まると思います。