0
1

React + API Gateway + Lambda + DynamoDBでTodoアプリ

Last updated at Posted at 2023-11-11

React

フロントエンドはReactで適当にTodoアプリを作っておきました。
process.env.REACT_APP_BASE_URL にはこの後作成するAPI Gatewayのエンドポイントが入ります。

App.js
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;
AddForm.jsx
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
Todo.jsx
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処理を作成します。

getTodo.rb
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
createTodo.rb
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
updateTodo.rb
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
deleteTodo.rb
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.rbget_next_idメソッドで次のidを取得する処理も実装しています。
DynamoDBのsequenceテーブルのseqカラムを使って実現しています。

SAM

AWS SAMを使ってデプロイするために、API Gateway, Lambda, DynamoDBのtempleteファイルを作成します。

templete.yml
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リソースを作成する前に一回マネジメントコンソール画面から手動でリソースを作成するなどするとより理解が深まると思います。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1