AWSでサーバーレスな「未達成なら寄付Todoアプリ」を作ってみた #1(設計〜バックエンド基礎編)
はじめに
AWS SAA(Solutions Architect Associate)の学習を兼ねて、以下のようなアプリを個人開発することにしました。
- タスクに「期限」「重要度(小/中/大)」を設定する
- 期限までに達成できなければ、重要度に応じた金額(1,000円/5,000円/10,000円)が自動で寄付として引き落とされる
- 判定は自己申告(完了ボタンを押すだけ)
いわゆる「セルフコミットメント系アプリ」ですが、実際に手を動かしながらAWSのサーバーレス構成(Lambda / API Gateway / DynamoDB / Cognito)を一通り触れるので、学習素材としてちょうどよいボリュームだと感じています。
この記事はシリーズ1本目、設計〜バックエンドの基礎API(作成・取得・完了)ができるまでの記録です。
全体構成
決済(Stripe)以外はすべてAWSのサーバーレスサービスで構成しています。「使った分だけ課金される」ため、個人開発でもコストを抑えられるのが利点です。

ブラウザ → S3+CloudFront → API Gateway → Lambda → DynamoDB が主要な流れ。Cognitoが認証、EventBridgeが定期実行、Stripeが寄付の決済を担当。
- フロントエンド:S3 + CloudFront で配信するReact製SPA(まだ未着手)
- 認証:Cognitoでユーザープールを作成(ログイン画面はホストされたUIを利用)
- バックエンド:API Gateway(HTTP API)+ Lambda(Node.js)
- データ:DynamoDB(オンデマンドモード)
- 決済:Stripe(カード情報は一切自前のシステムで扱わない設計。未達成が確定したタスクの金額を寄付として引き落とす)
DynamoDBのテーブル設計
複数人利用への拡張も見据えて、最初からuserIdを持たせる設計にしました。
| テーブル | パーティションキー | 主な項目 |
|---|---|---|
| Users | userId | stripeCustomerId, email |
| Tasks | taskId | userId, title, dueDate, importance, penaltyAmount, status, createdAt |
| PaymentHistory | paymentId | userId, taskId, amount, stripePaymentIntentId, chargedAt |
importance(重要度)からpenaltyAmount(寄付額)への変換は、Lambda側で以下のような対応表を持たせて自動計算しています。
const importanceToPenalty = {
small: 1000,
medium: 5000,
large: 10000,
};
作ったLambda関数(3本)
1. createTask - タスク作成
POSTで受け取った内容をDynamoDBに保存します。taskIdはcrypto.randomUUID()でサーバー側で発行。
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
export const handler = async (event) => {
const body = JSON.parse(event.body);
const taskId = randomUUID();
const createdAt = new Date().toISOString();
const importanceToPenalty = { small: 1000, medium: 5000, large: 10000 };
const params = {
TableName: "Tasks",
Item: {
taskId,
userId: body.userId,
title: body.title,
dueDate: body.dueDate,
importance: body.importance,
penaltyAmount: importanceToPenalty[body.importance],
status: "pending",
createdAt,
},
};
try {
await docClient.send(new PutCommand(params));
return {
statusCode: 201,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ taskId, message: "タスクを作成しました" }),
};
} catch (error) {
console.error(error);
return {
statusCode: 500,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "エラーが発生しました" }),
};
}
};
2. getTasks - タスク一覧取得
まずはシンプルにScanで全件取得し、Lambda側でuserIdをフィルタする実装にしました(パーティションキーがtaskIdのため、userId検索にはGSIが必要になりますが、これは今後の改善課題としています)。
const result = await docClient.send(new ScanCommand({ TableName: "Tasks" }));
const userTasks = result.Items.filter((task) => task.userId === userId);
メモ: DynamoDBの検索には
Scan(全件走査)とQuery(索引を使った効率的な検索)があり、実務では基本Queryが推奨されます。今回はGSI設計を後回しにして、まずは動く形を優先しました。
3. completeTask - タスク完了
UpdateCommandを使い、指定したtaskIdのstatusだけを部分更新します。
const params = {
TableName: "Tasks",
Key: { taskId },
UpdateExpression: "SET #s = :newStatus",
ExpressionAttributeNames: { "#s": "status" },
ExpressionAttributeValues: { ":newStatus": "completed" },
};
statusはDynamoDBの予約語のため、#sというプレースホルダー経由で指定する必要がある点がハマりポイントでした。
つまずいたポイント:curlのクォート問題
completeTaskをテストした際、以下のエラーに遭遇しました。
"errorMessage": "\"undefined\" is not valid JSON"
原因は、Windowsのコマンドプロンプトではシングルクォート'が正しく解釈されないことでした。Mac/Linuxのターミナルでは動くコマンドが、Windowsではevent.bodyがundefinedのまま届いてしまいます。
# Mac/Linuxはこれで動くが、Windows cmdでは失敗する
curl -X POST https://xxxx/completeTask -d '{"taskId":"xxxx"}'
# Windows cmdではダブルクォート+エスケープが必要
curl -X POST https://xxxx/completeTask -d "{\"taskId\":\"xxxx\"}"
環境依存の落とし穴として記録しておきます。
Lambdaの権限まわりで学んだこと
Lambda関数を新規作成すると、デフォルトではCloudWatch Logsへの書き込み権限しか付与されません(最小権限の原則)。DynamoDBを操作するには、関数ごとに作成されるIAMロールへ個別にAmazonDynamoDBFullAccessポリシーをアタッチする必要があります。
重要なのは、Lambda関数ごとにIAMロールが別々に作られるという点です。1つの関数で権限を追加しても、他の関数には引き継がれないため、createTask→getTasks→completeTaskと作るたびに同じ作業を3回繰り返しました。
現在の進捗
- DynamoDB 3テーブル作成(Users / Tasks / PaymentHistory)
- Cognitoユーザープール作成
-
createTaskLambda(動作確認済み) -
getTasksLambda(動作確認済み) -
completeTaskLambda(動作確認済み) -
checkOverdueTasks(期限切れ判定) - EventBridge Scheduler設定
- Stripe連携(カード登録・自動課金)
- フロントエンド(React + Cognito連携)
- API GatewayのセキュリティをCognito認証に変更
次回予告
次回は、EventBridge Schedulerで期限切れタスクを毎日自動検知し、Stripeで実際に寄付を実行する仕組みを実装していきます。