0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Router v7(SPA Mode) + AWS LambdaでTodoアプリ作る(バックエンド編)

Posted at

AWS初心者が記事を書いています。

はじめに

React Router + Lambda で Todo アプリを作成します。

最終的な構成は以下の通りです。
名称未設定ファイル.drawio.png

構成概要

フロント

  • React Router の SPA を S3 でホストします。
  • CloudFront 経由で配信します。

バックエンド〜DB

  • Lambda で実装します。API Gateway を使って API 化します。
  • Cognite による認証認可を通った場合のみ API にアクセス化とします。
  • DB は RDS(PostgreSQL)を使います。

今回は バックエンド〜DB を実装します。


実装手順

1. VPC を作成する

Lambda と RDS を格納するための VPC を作成します。

VPCを作成 を選択します。
スクリーンショット 2025-12-07 16.26.43.png

設定

項目 設定
作成するリソース VPCなど
AZ 2
パブリックサブネット 0
プライベートサブネット 2
NATゲートウェイ なし
VPC エンドポイント なし

2. セキュリティグループを作成する

Lambda と RDS 間のやり取りをセキュアにするため、それぞれに設定するセキュリティグループを作成します。

作るもの

  • lambda-sg

    • Lambda のアウトバウンドを RDS のみに制限
  • rds-sg

    • RDS のインバウンドを Lambda のみに制限

2-1. lambda-sg を作成する

VPCのダッシュボードを開き、セキュリティグループ を選択します。
スクリーンショット 2025-12-07 16.31.39.png

セキュリティグループを作成 を選択します。
スクリーンショット 2025-12-07 16.36.33.png

設定

項目 設定
セキュリティグループ名 lambda-sg
説明 任意のテキスト
VPC 1 で作成したVPC

インバウンドルール

設定不要です。

アウトバウンドルール

2-2 で rds-sg を作成した後に対応可能です。

次のルールを追加します。

項目 設定
タイプ PostgreSQL
送信先 rds-sg

2-2. rds-sg を作成する

次の設定で作成します。

項目 設定
セキュリティグループ名 rds-sg
説明 任意のテキスト
VPC 1 で作成したVPC

インバウンドルール

次のルールを追加します。

項目 設定
タイプ PostgreSQL
ソース lambda-sg

アウトバウンドルール

設定不要です。


3. RDS を作成する

Aurora and RDS のダッシュボードを開き、データベースを作成する を選択します。
スクリーンショット 2025-12-07 16.48.02.png

設定

項目 設定
エンジンのタイプ PostgreSQL
テンプレート 開発/テスト
デプロイオプション シングルAZ DBインスタンスデプロイ
DB インスタンス識別子 任意の識別子
認証情報管理 セルフマネージド
マスターパスワード お好きなパスワード
DBインスタンスクラス db.t4g.micro
※動作検証用なので一番軽いやつで
ストレージタイプ 汎用SSD
コンピューティングリソース EC2に接続しない
VPC 1で作成したVPC
パブリックアクセス なし
VPCセキュリティグループ 既存の選択
既存の VPC セキュリティグループ rds-sg
アヴェイラビリティゾーン 指定なし
RDS Proxy チェックなし
最初のデータベース名 お好きなDB名

最初のデータベース名追加設定 を展開したところで設定できます。
ここを指定していないと、最初のDBを自分で作成しないといけないので注意です。
スクリーンショット 2025-12-07 16.56.24.png


4. Lambda を作成する

次の用途の Lambda をそれぞれ作成します。

  • DB にテーブルを作成する Lambda
  • バックエンドとして動かす Lambda

4-1. DB にテーブルを作成する Lambda

todos テーブルを作成します。

テーブル定義

  • id: bigserial
  • title: text
  • completed bool
  • created_at: timestamp

4-1-1. ファイル用意(ローカル)

ローカル環境にて、以下の構成でファイルを用意します。

- createTable
  - index.js
  - package.json
index.js
const { Client } = require("pg");

const MIGRATIONS = [
  {
    id: "001_create_todos",
    sql: `
      create table if not exists todos (
        id bigserial primary key,
        title text not null,
        completed boolean not null default false,
        created_at timestamptz not null default now()
      );
      create index if not exists idx_todos_completed on todos(completed);
      create index if not exists idx_todos_created_at on todos(created_at desc);
    `,
  },
];

function dbConfig() {
  return {
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT || "5432"),
    database: process.env.DB_NAME,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    ssl: { rejectUnauthorized: false },
  };
}

exports.handler = async () => {
  const client = new Client(dbConfig());
  await client.connect();

  try {
    await client.query(`
      create table if not exists schema_migrations (
        id text primary key,
        applied_at timestamptz not null default now()
      );
    `);

    const { rows } = await client.query("select id from schema_migrations");
    const applied = new Set(rows.map((r) => r.id));

    const toApply = MIGRATIONS.filter((m) => !applied.has(m.id));
    for (const m of toApply) {
      await client.query("begin");
      try {
        await client.query(m.sql);
        await client.query("insert into schema_migrations(id) values($1)", [m.id]);
        await client.query("commit");
        console.log(`applied: ${m.id}`);
      } catch (e) {
        await client.query("rollback");
        throw e;
      }
    }

    return {
      ok: true,
      applied: toApply.map((m) => m.id),
      skipped: MIGRATIONS.filter((m) => applied.has(m.id)).map((m) => m.id),
    };
  } finally {
    await client.end();
  }
};
package.json
{
  "name": "create-table",
  "private": true,
  "type": "commonjs",
  "dependencies": {
    "pg": "^8.0.0"
  }
}

作成したら以下のコマンドを実行します。

npm i --omit=dev
zip -r function.zip .

作成した zip ファイルは後ほど使います。


4-1-2. Lambda 作成(AWS)

Lambda のダッシュボードを開き、関数の作成 を選択します。
スクリーンショット 2025-12-07 17.05.06.png

作成時の設定
項目 設定
関数名 お好きな関数名
ランタイム Node.js 24.x
VPC 有効化して、1で作成したVPCを選択
サブネット 1で作ったサブネットを選択
セキュリティグループ lambda-sg

関数作成後、アップロード元.zipファイル を選択し、先ほど作成した zip ファイルをアップロードします。
スクリーンショット 2025-12-07 17.08.22.png


4-1-3. 環境変数の設定

アップロード後、設定 > 環境変数 に以下の項目を設定します。
スクリーンショット 2025-12-07 17.07.55.png

項目 設定
DB_HOST DBインスタンスのエンドポイント
※データベースインスタンスの接続とセキュリティ から確認できます。
スクリーンショット 2025-12-07 17.12.16.png
DB_NAME DBインスタンス作成時に指定したDB名
DB_USER インスタンス作成時に指定したユーザ名
DB_PASSWORD インスタンス作成時に指定したパスワード
DB_PORT 5432

4-1-4. 動作確認

設定完了後、テストタブから実行してみます。
スクリーンショット 2025-12-07 17.14.22.png

結果が成功なら OK です。
スクリーンショット 2025-12-07 17.15.12.png

この関数は今後使わないので、ここで削除して OK です。


4-2. バックエンドとして動かす Lambda

もう一つの Lambda 関数を作成します。こちらはアプリケーションのバックエンドとして動くものになります。

4-2-1. ファイル作成(ローカル)

4-1 と同じ階層でファイルを作成します。
package.json は 4-1 と同様です。

index.js は以下の通りです。

index.js
const { Pool } = require("pg");

let pool;

function getPool() {
  if (!pool) {
    pool = new Pool({
      host: process.env.DB_HOST,
      port: Number(process.env.DB_PORT || "5432"),
      database: process.env.DB_NAME,
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      ssl: { rejectUnauthorized: false }
    });
  }
  return pool;
}

function json(statusCode, obj) {
  return {
    statusCode,
    headers: { "content-type": "application/json" },
    body: JSON.stringify(obj)
  };
}

exports.handler = async (event) => {
  const method = event.requestContext?.http?.method || "GET";
  const path = event.rawPath || "/";

  try {
    const p = getPool();

    if (method === "GET" && path === "/api/todos") {
      const { rows } = await p.query(
        "select id, title, completed, created_at from todos order by id desc limit 200"
      );
      return json(200, { items: rows });
    }

    if (method === "POST" && path === "/api/todos") {
      const body = event.body ? JSON.parse(event.body) : {};
      const title = String(body.title || "").trim();
      if (!title) return json(400, { message: "title is required" });

      const { rows } = await p.query(
        "insert into todos(title) values($1) returning id, title, completed, created_at",
        [title]
      );
      return json(201, { item: rows[0] });
    }

    const m = path.match(/^\/api\/todos\/(\d+)$/);
    if (m && method === "PATCH") {
      const id = Number(m[1]);
      const body = event.body ? JSON.parse(event.body) : {};
      const completed = Boolean(body.completed);
      const { rows } = await p.query(
        "update todos set completed=$2 where id=$1 returning id, title, completed, created_at",
        [id, completed]
      );
      if (!rows[0]) return json(404, { message: "not found" });
      return json(200, { item: rows[0] });
    }

    if (m && method === "DELETE") {
      const id = Number(m[1]);
      await p.query("delete from todos where id=$1", [id]);
      return json(204, {});
    }

    return json(404, { message: "not found" });
  } catch (e) {
    console.error(e);
    return json(500, { message: "internal error" });
  }
};

4-2-2. zip 化・Lambda 作成

zip を作成したら、4-1 と同じ手順で関数を作成してアップロードします。
環境変数も同様に設定します。


5. API Gateway を作成する

バックエンド用の Lambda 関数を API 化するため、API Gateway を作成します。
また、Cognito による認証認可も実装します。


5-1. API Gateway を作成する

API Gateway のダッシュボードを開き、APIを作成 を選択します。
スクリーンショット 2025-12-07 17.24.33.png

API タイプは「HTTP API」を選択します。
スクリーンショット 2025-12-07 17.25.36.png

統合 から、4-2 で追加した Lambda 関数を設定します。
スクリーンショット 2025-12-07 17.30.58.png

ルートは以下の通り設定します。
スクリーンショット 2025-12-07 17.33.18.png

作成後、Stages を選択して API のエンドポイントを確認できます。
スクリーンショット 2025-12-07 17.35.24.png

この状態で、ブラウザから https://<APIエンドポイント>/api/todos にアクセスすると、DB に登録されている todo の一覧を取得できます。
スクリーンショット 2025-12-07 17.37.17.png

ただし、今は無条件にアクセスできてしまうので、Cognito による認証認可を追加します。


5-2. ユーザプールとアプリケーションクライアントを作成する

Cognito のダッシュボードから、ユーザープールを作成 を選択します。
スクリーンショット 2025-12-07 17.45.28.png

設定

項目 設定
アプリケーションタイプ SPA
サインイン識別子のオプション メールアドレス ※それ以外でも可
自己登録 オン
サインアップのための必須属性 なし
リターン URL 入力しない

ユーザプールとアプリケーションが作成されます。
スクリーンショット 2025-12-07 17.53.04.png
スクリーンショット 2025-12-07 17.52.32.png

テストユーザを追加

この後の動作確認のため、ユーザプールにテストユーザを追加しておきます。

ユーザーを作成 を選択します。
スクリーンショット 2025-12-07 18.00.54.png

メールアドレスを入力し、適当なパスワードを設定しておきます。
スクリーンショット 2025-12-07 18.03.00.png


5-3. API Gateway にオーサライザーを設定する

オーサライザーとは

API Gateway によって呼び出される認可チェックの仕組みです。
API を呼ぼうとするクライアントからのリクエストを受けたとき、オーサライザーを通すことで「このクライアントがこの API を呼んでよいか」を判断できます。

API Gateway に戻り、Authorization > オーソライザーを管理 > 作成 を選択します。
スクリーンショット 2025-12-07 18.05.15.png

設定

項目 設定
オーソライザーのタイプ JWT
名前 任意の名前
IDソース $request.header.Authorization
発行者URL https://cognito-idp.<リージョン>.amazonaws.com/<5-2で作成したユーザプールのユーザプールID>
対象者 5-2 で作成したアプリケーションクライアントのクライアントID

オーソライザーをルートにアタッチ から、各ルートにアタッチします。
スクリーンショット 2025-12-07 18.13.45.png

これで、API のアクセスに認可が必須になりました。
試しに 5-1 と同様に API エンドポイントへアクセスすると、Unauthorized でエラーとなります。
スクリーンショット 2025-12-07 18.16.43.png


5-4. アクセストークンを取得して API にアクセスする

API にアクセスするにはアクセストークンが必要です。
そして、アクセストークンを取得するには、ユーザ認証を行って認可コードを取得する必要があります。

5-4-1. 認可コードを取得する

ユーザ認証を行うため、アプリケーションクライアントのダッシュボードを開き、ログインページを表示 を選択します。
スクリーンショット 2025-12-07 18.21.05.png

表示された画面で、メールアドレスとパスワードを入力します。
スクリーンショット 2025-12-07 11.39.31.png

サインイン後、今回はパスワード強制変更する設定にしていたので、新パスワードを入力します。
スクリーンショット 2025-12-07 11.42.18.png

サインイン完了画面が表示されます。アドレスバーで、認可コードを確認できます。
スクリーンショット 2025-12-07 18.23.29.png


5-4-2. 認可コードからトークンを取得する

取得した認証コードを使ってトークンを取得します。

Cognite プレフィックスドメイン は、ユーザプールのダッシュボードから 認証方法 を選択し、パスキー の項目から確認できます。
スクリーンショット 2025-12-07 18.29.32.png

curl -sS -X POST "https://<Cognito プレフィックスドメイン>/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=<クライアントID>" \
  --data-urlencode "code=<取得した認可コード>" \
  --data-urlencode "redirect_uri=<redirect_url>"

うまくいくと、アクセストークンや ID トークンが返ってきます。


5-4-3. アクセストークンで API にアクセスする

取得したアクセストークンを使って API エンドポイントにアクセスします。

curl -sS '<APIエンドポイント>/api/todos' \
 -H "Authorization: Bearer <取得したアクセストークン>"

以下のようなレスポンスが返ってくれば OK です。

"items":[]

おわりに

以上で、バックエンドの実装はひとまず完了です。
次回はフロントエンドアプリを作成していきます。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?