AWS

API Gateway + Lambda + DynamoDBでサーバレスなTodoアプリ作成

NEW 2026年8月18日

目次Category


はじめに

今回はAWSの3つのサービスを組み合わせた サーバーレスな Todo アプリを作ります
EC2インスタンスを立てずに、マネージドサービスだけで構築します

サービス役割
Lambdaサーバーが不要でリクエスト時にプログラムを実行するサービス
API GatewayAPIの作成・管理ができるサービス
DynamoDBフルマネージド型のNoSQLデータベース
前提条件
  • 操作・確認は AWS コンソール(Web) で進めます
  • Python 3.14 を使用します(Lambda ランタイム)
  • VPC 設定は不要です(Lambda を VPC 外に置くため不要)

今回構築する全体像

flow.txt
Phase 1  Lambda 作成(モック)→ 単体テスト
↓
Phase 2  API Gateway 接続 → GET/POST 確認
↓
Phase 3  DynamoDB 接続 → 永続化
↓
Phase 4  ローカル HTML から操作

アーキテクチャ図

構築手順

Step1 Lambda の作成

まず Lambda 関数を作成し、DynamoDB の代わりにメモリ上の辞書で動作確認します

① Lambda 関数の作成

AWS コンソールから Lambda を開き、関数の作成を選択

設定内容
項目
関数名ojt-lambda-name-01(自分の名前に合わせて変更)
ランタイムPython 3.14
その他デフォルトのまま

Lambda 関数の作成

関数の作成後、コードエディタに以下のコードを貼り付けてデプロイ

今回はテストなのでDynamoDB は使用せず、メモリをデータベースとして使用します

lambda_handler_mock.py
import json

# 本物のDynamoDBの代わりに、メモリ上の辞書(Dict)をデータベースとして使用
# 構造: { "ユーザー名": ["タスク1", "タスク2", ...] }
MOCK_DYNAMODB = {
    "yamada": ["AWSの勉強をする", "日報を書く"],
    "tanaka": ["Pythonのコードレビュー"],
}

def lambda_handler(event, context):
    """
    API Gatewayからのリクエストを処理するハンドラー
    """
    # 1. API Gatewayからのリクエスト情報を取得
    # ※API Gatewayの**Lambda統合プロキシ**を想定
    http_method = event.get("httpMethod")
    path_parameters = event.get("pathParameters") or {}
    query_parameters = event.get("queryStringParameters") or {}
    
    # ルーティング処理
    try:
        # 【タスク取得】GET /todo?user=ユーザー名
        if http_method == "GET":
            username = query_parameters.get("user")
            if not username:
                return create_response(400, {"message": "ユーザー名(user)を指定してください"})
            
            # DynamoDB(モック)からユーザー名(PK)で検索
            tasks = MOCK_DYNAMODB.get(username, [])
            return create_response(200, {"user": username, "tasks": tasks})

        # 【タスク追加】POST /todo
        elif http_method == "POST":
            # ボディの解析
            body = json.loads(event.get("body", "{}"))
            username = body.get("user")
            task = body.get("task")
            
            if not username or not task:
                return create_response(400, {"message": "user と task は必須です"})
            
            # DynamoDB(モック)へデータ保存(ユーザーが未登録なら新規作成)
            if username not in MOCK_DYNAMODB:
                MOCK_DYNAMODB[username] = []
            MOCK_DYNAMODB[username].append(task)
            
            return create_response(201, {
                "message": "タスクを追加しました",
                "current_tasks": MOCK_DYNAMODB[username]
            })

        # 対応していないメソッド
        else:
            return create_response(405, {"message": f"Method {http_method} Not Allowed"})

    except Exception as e:
        return create_response(500, {"message": f"Internal Server Error: {str(e)}"})


def create_response(status_code, body_content):
    """
    API Gateway(プロキシ統合)が認識できる形式のレスポンスを作成するヘルパー関数
    CORS対策のヘッダーもあらかじめ付与
    """
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",  # フロントエンド(HTML)からのクロスドメイン通信を許可
            "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
            "Access-Control-Allow-Headers": "Content-Type"
        },
        "body": json.dumps(body_content, ensure_ascii=False)
    }

コードを貼り付けたらDeployボタンを押す

Deploy 成功
関数 ~~~~ が正常に更新されましたと出れば成功です

② テストイベントで動作確認する(GET・POST)

コードが正しく動くかを、API Gateway を通さずに Lambda 単体でテストします

テストイベント 1:GET のテスト

テストタブから新しいイベントを作成を選択し、以下を入力して保存

  • イベント名:getTest

getTest.json
{
  "httpMethod": "GET",
  "queryStringParameters": {
    "user": "yamada"
  }
}

GET テストイベント

テストを実行して、yamada のタスク一覧が返ってくることを確認

GET テスト結果
statusCodeが200になっていて、bodyに上記の内容が表示されていれば成功

テストイベント 2:POST のテスト
  • イベント名:postTest

postTest.json
{
  "httpMethod": "POST",
  "body": "{\"user\": \"yamada\", \"task\": \"課題を終わらせる\"}"
}

テストを実行して、タスクの追加が成功することを確認

POST テスト結果
statusCodeが201になっていて、bodyに上記の内容が表示されていれば成功

Step2 API Gateway との接続

Lambda を API として公開するため、API Gateway を作成して接続します

① API Gateway の作成

AWS コンソールから API Gateway を開き、REST API構築を選択

設定内容
項目
API 名ojt-api-name-01(自分の名前に合わせて変更)
その他デフォルトのまま

API Gateway 作成 1
API Gateway 作成 2

② リソース(/todo)の作成

作成した API を開き、リソースの作成を選択

設定内容
項目
リソース名todo
クロスオリジンリソース共有(CORS)チェックを入れる

リソース作成

③ GET・POST メソッドの作成

/todo リソースを選択し、メソッドの作成から GET と POST をそれぞれ追加

GET メソッドの設定
項目
メソッドタイプGET
統合タイプLambda 関数
Lambda プロキシ統合✅ チェックを入れる
Lambda 関数作成した Lambda 関数を選択

GET メソッド設定

POST メソッドの設定

GET と同じく、メソッドタイプのみ POST を選択

④ API のデプロイ(dev ステージ)

API をデプロイを選択し、以下の設定でデプロイ

設定内容
項目
ステージ新しいステージ
ステージ名dev

API デプロイ

デプロイ後、呼び出し URLが表示されるのでコピーしておきます

呼び出し URL

⑤ ブラウザと curl で動作確認

ブラウザで GET の確認

先ほどコピーしたURLにtodoのパスと引数を加えます

例)
https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/todo?user=yamada

yamada のタスク一覧が JSON で返ってくることを確認

ブラウザ GET 確認

curl で POST の確認

実行する環境(OS・シェル)に合わせて、以下の curl コマンドをターミナルで実行します
xxxxxxxxは自身のURLに書き換えてください

Mac / Linux / WSL / Git Bash の場合

Terminal (Mac/Linux)
curl -X POST https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/todo \
  -H "Content-Type: application/json" \
  -d '{"user": "yamada", "task": "本物のDynamoDBに保存する"}'
Windows コマンドプロンプト(cmd.exe)の場合

Terminal (Windows cmd)
curl -X POST https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/todo ^
  -H "Content-Type: application/json" ^
  -d "{\"user\": \"yamada\", \"task\": \"本物のDynamoDBに保存する\"}"

curl POST 結果

Step3 DynamoDB との接続

メモリ上のモックから、本物の DynamoDB に切り替えてデータを永続化します

① DynamoDB でテーブルを作成

AWS コンソールから DynamoDB を開き、テーブルの作成を選択

設定内容
項目
テーブル名ojt-dynamo-name-01(自分の名前に合わせて変更)
パーティションキー(PK)user(文字列)
ソートキー(SK)timestamp(文字列)
その他デフォルトのまま

DynamoDB テーブル作成

② Lambda に DynamoDB の権限を付与

Lambda はデフォルトでDynamoDB を操作できないので
IAM ロールにポリシーをアタッチして、権限を付与します

設定手順
  1. Lambda の設定タブ → アクセス権限を開く
  2. ロール名のリンクをクリック(IAM コンソールが開く)
  3. 許可を追加ポリシーをアタッチを選択
  4. AmazonDynamoDBFullAccess を検索して選択 → 許可を追加

IAM ロール
ポリシーアタッチ

③ コードを DynamoDB 連携版に書き換え

Lambda のコードエディタを開き、以下のコードに差し替えてデプロイ
テスト版はLambdaのメモリで操作しましたが、今回はDynamoDBで操作します
TABLE_NAMEを自身のテーブル名に変えてください

lambda_handler_dynamodb.py
import json
import boto3
from datetime import datetime, timezone, timedelta
from boto3.dynamodb.conditions import Key

# DynamoDBオブジェクトの初期化
dynamodb = boto3.resource('dynamodb')
TABLE_NAME = 'ojt-dynamo-name-01'  # ← 自身で作成したテーブル名に変更
table = dynamodb.Table(TABLE_NAME)

def lambda_handler(event, context):
    """
    PK(user) と SK(timestamp) を使用した、実際のDynamoDB接続コード
    """
    http_method = event.get("httpMethod")
    query_parameters = event.get("queryStringParameters") or {}
    
    try:
        # 【タスク取得】GET /todo?user=ユーザー名
        if http_method == "GET":
            username = query_parameters.get("user")
            if not username:
                return create_response(400, {"message": "ユーザー名(user)を指定してください"})
            
            # ソートキーがある場合、get_itemではなく**query**を使う
            # 指定したユーザー(PK)のデータを全件検索
            response = table.query(
                KeyConditionExpression=Key('user').eq(username)
            )
            
            items = response.get('Items', [])
            
            # フロントエンド(JS)が扱いやすいように配列にして返す
            tasks_list = []
            for item in items:
                tasks_list.append({
                    "timestamp": item.get("timestamp"),
                    "task": item.get("task")
                })
            
            return create_response(200, {"user": username, "tasks": tasks_list})

        # 【タスク追加】POST /todo
        elif http_method == "POST":
            body = json.loads(event.get("body", "{}"))
            username = body.get("user")
            task = body.get("task")
            
            if not username or not task:
                return create_response(400, {"message": "user と task は必須です"})
            
            # 現在時刻をソートキー(SK)にする(重複を防ぎ、並び替えを容易にするため)
            # 日本時間(JST)のタイムスタンプ文字列(例: 2026-07-07T12:00:00)
            jst = timezone(timedelta(hours=+9))
            now_str = datetime.now(jst).isoformat()
            
            # PK(user)とSK(timestamp)の組み合わせが新しいので、上書きされずに別データとして保存
            table.put_item(
                Item={
                    'user': username,
                    'timestamp': now_str,
                    'task': task
                }
            )
            
            return create_response(201, {
                "message": "タスクを追加しました",
                "added_task": {
                    "user": username,
                    "timestamp": now_str,
                    "task": task
                }
            })

        # 対応していないメソッド
        else:
            return create_response(405, {"message": f"Method {http_method} Not Allowed"})

    except Exception as e:
        return create_response(500, {"message": f"DynamoDB Error: {str(e)}"})


def create_response(status_code, body_content):
    """API Gateway(プロキシ統合)用レスポンス作成ヘルパー"""
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
            "Access-Control-Allow-Headers": "Content-Type"
        },
        "body": json.dumps(body_content, ensure_ascii=False)
    }

④ テストイベントで動作確認

POST テスト

以前作成した postTest イベントを選択してテストを上書き保存して実行

postTest.json
{
  "httpMethod": "POST",
  "body": "{\"user\": \"yamada\", \"task\": \"本物のDynamoDBに保存する\"}"
}

DynamoDB POST テスト結果

DynamoDB コンソールの項目を探索でテーブルを開き、データが追加されていることを確認

DynamoDB 項目確認

Step4 ローカル HTML からの接続

ブラウザから API を呼び出すためのシンプルな HTML ファイルを作成します

① todo.html の作成

HTML・CSS・JavaScript をひとつのファイルにまとめた構成

todo.html 内の API_URL を、自分のエンドポイント URL(/todo まで)に変更してください。

todo.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>サーバーレス Todoリスト</title>
</head>
<body>
<div class="container">
    <h1>📝 Todo リスト</h1>

    <!-- ユーザー名入力 + 検索ボタン -->
    <div class="form-group">
        <input type="text" id="username" placeholder="ユーザー名を入力(例: yamada)">
        <button class="search-btn" onclick="fetchTodos()">検索</button>
    </div>

    <hr>

    <!-- タスク入力 + 追加ボタン -->
    <div class="form-group">
        <input type="text" id="taskContent" placeholder="新しいタスクを入力">
        <button onclick="addTodo()">追加</button>
    </div>

    <ul id="todoList"></ul>
</div>

<script>
// TODO: あなたのAPI GatewayのURL(/todoまで)に書き換えてください
const API_URL = "【ここにあなたのAPI GatewayのURLを貼り付ける】";

// 1. 【GET】ユーザー名で検索してタスク一覧を取得する関数
async function fetchTodos() {
    const username = document.getElementById("username").value.trim();
    if (!username) {
        alert("ユーザー名を入力してください");
        return;
    }
    try {
        const response = await fetch(`${API_URL}?user=${encodeURIComponent(username)}`);
        if (!response.ok) throw new Error("データの取得に失敗しました");
        const data = await response.json();
        displayTodos(data.tasks);
    } catch (error) {
        alert("エラーが発生しました: " + error.message);
    }
}

// 2. 【POST】新しいタスクを追加する関数
async function addTodo() {
    const username = document.getElementById("username").value.trim();
    const taskContent = document.getElementById("taskContent").value.trim();
    if (!username || !taskContent) {
        alert("ユーザー名とタスクの両方を入力してください");
        return;
    }
    try {
        const response = await fetch(API_URL, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ user: username, task: taskContent })
        });
        if (!response.ok) throw new Error("タスクの追加に失敗しました");
        document.getElementById("taskContent").value = "";
        fetchTodos();
    } catch (error) {
        alert("エラーが発生しました: " + error.message);
    }
}

// 3. タスク配列を画面(HTML)に描画するヘルパー関数
function displayTodos(tasks) {
    const todoListUl = document.getElementById("todoList");
    todoListUl.innerHTML = "";
    if (tasks.length === 0) {
        todoListUl.innerHTML = "<li>タスクはありません</li>";
        return;
    }
    tasks.forEach(item => {
        const li = document.createElement("li");
        const displayTime = item.timestamp
            ? item.timestamp.substring(0, 16).replace("T", " ")
            : "";
        li.innerHTML = `
            <span>${escapeHtml(item.task)}</span>
            <span class="timestamp">${displayTime}</span>
        `;
        todoListUl.appendChild(li);
    });
}

// 4. セキュリティ対策(XSS対策):文字エスケープ関数
function escapeHtml(str) {
    return str.replace(/[&<>"']/g, match => {
        const escape = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": ''' };
        return escape[match];
    });
}
</script>
</body>
</html>

② ブラウザで動作確認

todo.html をブラウザで開き、以下の流れで動作確認

確認手順
  1. ユーザー名に yamada と入力して検索→ タスク一覧が表示されることを確認
  2. タスク入力欄に文字を入力して追加→ リストに反映されることを確認
  3. DynamoDBの設定画面で値を追加しhtmlで反映されているか確認

まとめ

これで作成は以上です
削除できるサービスは消しておきましょう

片付け

演習が終わったら、不要なリソースを削除します

  • Lambda
  • API Gateway
  • DynamoDB

この記事を書いた人

宮本歩

ayumu_miyamoto

26卒 宮本です
趣味でCloudflareを触っています