Cloudflare Workers + react router v7 でサクサクWebアプリを作る。

Cloudflare Workers + react router v7 でサクサクWebアプリを作る。

Clock Icon2025.05.07

はじめに

皆様こんにちは、あかいけです。

最近ポートフォリオを改修したのですが、その際に Cloudflare Workers + react router v7 の組み合わせを使ってみました。

https://about.lamaglama39.dev/

その結果アプリの拡張性やデプロイの簡易性などもろもろ開発体験がめちゃくちゃよかったので、ブログにしました。
まだCloudflareを使ってない人も、これを機に入門してみてください。

Cloudflare Workers is 何?

Cloudflare Workers は、Cloudflare のエッジサーバーで JavaScript/TypeScript/Python/Rust/WASM を動かすサーバーレス実行環境です。

https://www.cloudflare.com/ja-jp/developer-platform/products/workers/
https://www.cloudflare.com/ja-jp/learning/serverless/serverless-javascript/

すごく大雑把に言うと、「CDN にアプリロジックを埋め込んだ」ようなもの…。
それが Cloudflare Workers です 。

類似のサービスを上げるとすれば、AWS で言えば Lambda@Edge、
Google Cloud で言えば Cloud Functions + Cloud CDN(または Cloud Run + CDN)が近いでしょう。

ただし、Cloudflare Workers はCDN ネットワークのエッジで直接 JavaScript を実行できるという点で、よりCDN 寄りのサーバレスです。
例を挙げると、以下のような使い方が得意です。

  • APIのレスポンスをエッジで加工する
  • 認証やBot判定のプリフィルターを入れる
  • 小規模なフロントエンドをそのまま動かす

ただし、その反面いくつかの制限もあります。

https://developers.cloudflare.com/workers/platform/limits/

  • CPU実行時間
    • 無料プラン:10 ms
    • 有料プラン:5 min HTTP request / 15 min Cron Trigger
  • メモリ
    • 最大で128MB (無料プラン/有料プラン共通)
  • サブリクエスト数
    • 無料プラン:1 リクエストあたり最大 50 件のサブリクエスト(fetch() 呼び出しなど)
    • 有料プラン:1 リクエストあたり最大 1,000 件のサブリクエスト

つまり、「がっつり計算処理をしたい」や「長時間のバッチ処理を回したい」といったユースケースには向いていません。

また以下は少し昔の記事ですが、
Cloudflare Workers のコンセプトと思想がわかるので気になる方は読んでみてください。

https://blog.cloudflare.com/ja-jp/cloudflare-workers-serverless-week/

React Router v7 is 何?

React Router は SPA/MPA 向けのルーティングライブラリですが、v7で Remix と統合され、またフレームワーク・モードを搭載し、
「ルーティング + データ取得 + SSR/プリレンダリング」 まで一気通貫で扱えるようになりました 。

https://remix.run/blog/react-router-v7

デプロイまでの手順

1 . アカウント作成

(1). Cloudflare アカウント

以下公式サイト右上の「サインアップ」からアカウントを作成してください。
何とアカウントの作成自体はクレジットカードの登録も必要ありません、素敵ですね。

https://www.cloudflare.com/ja-jp/

(2). ドメイン購入 (オプション)

Cloudflare Workers はデフォルトのドメインが用意されているので、
独自ドメインが使いたい場合にご購入してください。

https://www.cloudflare.com/ja-jp/products/registrar/

Cloudflare はドメイン名の原価販売を行っているので、
ドメインの費用を抑えたい方はここで購入するのもいいかなと思います。

https://www.cloudflare.com/ja-jp/application-services/solutions/low-cost-domain-names/

2 . 環境セットアップ

以降の手順は以下ドキュメントの通りです。

https://developers.cloudflare.com/workers/frameworks/framework-guides/react-router/

(1). Node.js インストール

以下のパッケージマネージャでインストールできます。

  • npm
  • pnpm
  • yarn

本記事では npm を使うので、Node.js がインストールされていれば OK です。
これからインストールする場合は mise でインストールすることをお勧めします。

https://mise.jdx.dev/getting-started.html

$ mise use nodejs@latest
$ node -v
v22.2.0

$ npm -v
10.7.0

(2). React + React Router v7 テンプレート作成

以下のコマンドで作成します。

$ npm create cloudflare@latest -- my-react-router-app --framework=react-router

Gitでのバージョン管理はYesにします。

╰ Do you want to use git for version control?
  Yes / No

まだデプロイしないので、ここはNoにします。

╰ Do you want to deploy your application?
  Yes / No

作成が完了したら移動します。

$ cd my-react-router-app/

3. デプロイ

(1). ローカル環境

ローカルサーバーでの確認は以下コマンドで出来ます。

$ npm run dev

実行後に、以下の URL からアクセスできます。

(2). プレビュー

次にプレビューは以下コマンドで出来ます。
ビルド後の状態を確認できるため、デプロイ前に確認するといいかもしれません。

$ npm run preview

実行後に、以下の URL からアクセスできます。

(3). 本番環境

本番環境へのデプロイは以下コマンドで出来ます。
初回実行であれば、Cloudflare Workers 作成 + デプロイまで実行されます。

$ npm run deploy

私の環境では約 10 秒ぐらいでデプロイが完了しました。 (めちゃ早い!)
実行後に表示される以下の形式の URL からアクセスできます。

  • https://<アプリ名>.<アカウント名>.workers.dev
Total Upload: 857.31 KiB / gzip: 168.20 KiB
Worker Startup Time: 2 ms
Your Worker has access to the following bindings:
- Vars:
  - VALUE_FROM_CLOUDFLARE: "Hello from Cloudflare"
Uploaded my-react-router-app (8.34 sec)
Deployed my-react-router-app triggers (0.80 sec)
  https://<アプリ名>.<アカウント名>.workers.dev
Current Version ID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

なおその他デフォルトで用意されているコマンドは、
package.json の scripts をご確認ください。

package.json
{
	"name": "my-react-router-app",
	"private": true,
	"type": "module",
	"scripts": {
		"build": "react-router build",
		"cf-typegen": "wrangler types",
		"deploy": "npm run build && wrangler deploy",
		"dev": "react-router dev",
		"preview": "npm run build && vite preview",
		"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
	},
	"dependencies": {
		"isbot": "^5.1.27",
		"react": "^19.1.0",
		"react-dom": "^19.1.0",
		"react-router": "^7.5.3"
	},
	"devDependencies": {
		"@cloudflare/vite-plugin": "^1.0.12",
		"@cloudflare/workers-types": "^4.20250506.0",
		"@react-router/dev": "^7.5.3",
		"@tailwindcss/vite": "^4.1.4",
		"@types/node": "^20",
		"@types/react": "^19.1.2",
		"@types/react-dom": "^19.1.2",
		"tailwindcss": "^4.1.4",
		"typescript": "^5.8.3",
		"vite": "^6.3.3",
		"vite-tsconfig-paths": "^5.1.4",
		"wrangler": "^4.14.1"
	}
}

色々設定してみる

さて、とりあえずデプロイできましたが、
これで終わってはつまらないので色々設定してみましょう。

ディレクトリ構成について

まずディレクトリ構成について説明します。
デフォルトの状態でかなりファイルが多いので圧倒されますが、頻繁に触るのは一部なのですべてを把握しなくても大丈夫です。

.
├── .git                       # Gitのリポジトリ情報(バージョン管理に必要)
├── .gitignore                 # Gitに含めないファイル・ディレクトリの指定
├── README.md                  # プロジェクトの概要や使い方の説明
├── app/                       # React Router のルートやコンポーネントの格納先
├── node_modules/              # npmでインストールされた依存パッケージ
├── package-lock.json          # npmの依存関係を固定するためのファイル
├── package.json               # 使用パッケージ・スクリプト・メタ情報
├── public/                    # 静的ファイルの格納先(favicon, robots.txt など)
├── react-router.config.ts     # React Router の設定情報(ルート設定など)
├── tsconfig.cloudflare.json   # Cloudflare Worker 用の TypeScript 設定
├── tsconfig.json              # 共通の TypeScript 設定ファイル
├── tsconfig.node.json         # Node.js環境向けの TypeScript 設定
├── vite.config.ts             # Vite(ビルドツール)の設定
├── worker-configuration.d.ts  # Cloudflare Worker 設定の型定義ファイル
├── workers/                   # Cloudflare Workers のエントリーポイントやロジック
└── wrangler.jsonc             # Cloudflare Wrangler の設定ファイル(デプロイ設定)

その中でも以下が特に重要なものです。

ファイル/ディレクトリ 説明
app/ React Router のルートやコンポーネントを格納。実際の UI やルーティングを記述する主要場所。
workers/ Cloudflare Worker のコード(API ロジックなど)。エッジで動く処理を記述。
wrangler.jsonc Wrangler によるデプロイ設定。Cloudflare の設定(KV やルートなど)もここで。
vite.config.ts フロントエンドの開発ビルド設定。Worker に合わせた最適化もここに含まれる。
package.json スクリプト実行 (dev, deploy, preview) や依存管理を行う中心ファイル。

https://developers.cloudflare.com/workers/wrangler/configuration/

カスタムドメイン名

wrangler.jsonc で設定できます。
以下のように利用したいドメイン名を記述して、デプロイすれば反映されます。

{
  "routes": [
    {
      "pattern": "test.example.com",
      "custom_domain": true
    }
  ]
}

デプロイ完了後、すぐにアクセスできるようになります。

Total Upload: 857.31 KiB / gzip: 168.20 KiB
Worker Startup Time: 3 ms
Your Worker has access to the following bindings:
- Vars:
  - VALUE_FROM_CLOUDFLARE: "Hello from Cloudflare"
Uploaded my-react-router-app (7.19 sec)
Deployed my-react-router-app triggers (1.48 sec)
  test.example.com (custom domain)
Current Version ID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

コンテンツ編集

編集前に npm run dev でローカル環境を立ち上げてブラウザに表示しておきます。
今回は例としてデフォルトページの app/welcome/welcome.tsx を編集します。
「What's next?」を「Enjoy React Router v7!!!」に修正しました。

        <div className="max-w-[300px] w-full space-y-6 px-4">
          <nav className="rounded-3xl border border-gray-200 p-6 dark:border-gray-700 space-y-4">
            <p className="leading-6 text-gray-700 dark:text-gray-200 text-center">
              Enjoy React Router v7!!!
            </p>

ホットリロードに対応しているので、ファイルを保存するとすぐに反映されます。

なお以下は、Cloudflare Workers + React Router v7 におけるリクエスト・レスポンスの大まかな流れ(SSR)です。
気になる方は見てみてください。

Cloudflare Workers + React Router v7 におけるリクエスト・レスポンス (SSR)

1. クライアントからのリクエスト

ユーザーがブラウザでURLにアクセスすると、そのリクエストはCloudflare のグローバルネットワークを経由してWorkerに届きます。

2. Cloudflare Workers の処理開始

wrangler.jsoncmain で指定されたエントリーポイントが実行されます。

wrangler.jsonc
	"main": "./workers/app.ts",

3. リクエストハンドラの起動

workers/app.ts 内で、React Router の createRequestHandler が呼び出され、リクエストの処理が開始されます
ここで virtual:react-router/server-build は、ビルド時に生成される仮想モジュールで、Vite のプラグインによって解決されるサーバーサイドルーターコードを参照しています。

const requestHandler = createRequestHandler(
  () => import("virtual:react-router/server-build"),
  import.meta.env.MODE
);

4. ルートマッチング

React Router がリクエストパスを解析し、app/routes.ts で定義されたルート構成と照合します。

import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
    index("routes/home.tsx"),
] satisfies RouteConfig;

5. ローダー関数の実行

マッチしたルートに定義されている loader 関数があれば実行され、データが取得されます。

6. サーバーサイドレンダリング

app/entry.server.tsx が呼び出され、以下の処理が順に行われます:

  • app/root.tsx (全体レイアウト) のレンダリング
  • マッチしたルートコンポーネントのレンダリング (例: app/routes/home.tsx)
  • 各コンポーネントが子コンポーネントをレンダリング (例: app/routes/home.tsx)

7. HTML 生成とレスポンス

レンダリング結果が必要なデータと共にクライアントに返されます。

8. クライアントサイド

ブラウザがHTMLを受け取ると、関連するJavaScriptが読み込まれ、
サーバーから送られたデータとマークアップに基づいて、Webページがが表示されます。

ページ追加

次にページを追加するには以下のファイルを追加する必要があります。
なおデフォルトページに合わせたルーティング方法なので、これはあくまで一例となります。
その他ルーティングの詳しい仕様については以下をご参照ください。

https://reactrouter.com/start/framework/routing

(1). app/test/test.tsx

最終的に表示されるコンテンツを作成します。

export function Test() {
  return (
    <div className="h-screen flex items-center justify-center">
      <div className="p-6 border border-gray-200 dark:border-gray-700 rounded-3xl">
        Test
      </div>
    </div>
  );
}

(2). app/routes/test.tsx

ルーティング用のファイルを作成します。

import type { Route } from "./+types/home";
import { Test } from "../test/test";

export function meta({}: Route.MetaArgs) {
  return [
    { title: "Test" },
    { name: "description", content: "Test" },
  ];
}

export default function Home() {
  return <Test />;
}

(3). app/routes.ts

route で対応するパス、対応するルートファイルを指定します。

import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
    index("routes/home.tsx"),
    route("/test", "routes/test.tsx")
] satisfies RouteConfig;

これで以下のURLでページが追加されます。

環境変数 と シークレット

次にCloudflare Workersで利用できる環境変数 と シークレットについてです。
なおどちらもCloudflare Workers内に閉じた設定です。

https://developers.cloudflare.com/workers/configuration/environment-variables/
https://developers.cloudflare.com/workers/configuration/secrets/

wrangler.jsonc

まずwrangler.jsoncに設定する場合です。
デフォルトファイルに以下の環境変数が設定されているので、こちらを例にします。

wrangler.jsonc
	"vars": {
		"VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare",
	},

Cloudflare Workers側では以下のように環境変数を参照できます。

app.ts
export interface Env {
  VALUE_FROM_CLOUDFLARE: string;
}

export default {
  async fetch(request, env, ctx): Promise<Response> {
    return new Response(`${env.VALUE_FROM_CLOUDFLARE}`);
  },
} satisfies ExportedHandler<Env>;

以下のように環境ごとに分離して環境変数を設定することもできます。

wrangler.jsonc
	"vars": {
		"VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare",
	},
	"env": {
		"staging": {
		  "vars": {
			"VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare Staging"
		  }
		},
		"production": {
		  "vars": {
			"VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare Production"
		  }
		}
	},

ただし Vite を組み合わせて利用する場合は、Vite 用の設定ファイルを作成する必要があります。

https://developers.cloudflare.com/workers/vite-plugin/reference/cloudflare-environments/

.env.staging
CLOUDFLARE_ENV=staging
.env.production
CLOUDFLARE_ENV=production

ビルド時のオプション指定にて、環境変数変数を指定できます。

$ npx vite build --mode staging
$ npx vite preview

ただしこの方法は設定ファイルに直書きとなり、
リポジトリにて保存することになるため、クレデンシャルな情報を設定するのはおすすめできません。

.dev.vars

.dev.varsはよく見る.envのようなものです。
以下のように環境変数を定義して、

.dev.vars
ENV_VALUE="value"

Cloudflare Workers側では以下のように環境変数を参照できます。

app.ts
export interface Env {
  ENV_VALUE: string;
}

export default {
  async fetch(request, env, ctx): Promise<Response> {
    return new Response(`${env.ENV_VALUE}`);
  },
} satisfies ExportedHandler<Env>;

シークレット

クレデンシャルな情報を共有する必要がある場合は、
こちらを利用することをおすすめします。

コマンドだと以下のように作成して、

$ npx wrangler secret put SECRET_VALUE

値を聞かれるので入力します。

 ⛅️ wrangler 4.14.1 (update available 4.14.2)
-------------------------------------------------------

? Enter a secret value: ›

Cloudflare Workers側では以下のようにシークレットを参照できます。

app.ts
export interface Env {
  SECRET_VALUE: string;
}

export default {
  async fetch(request, env, ctx): Promise<Response> {
    return new Response(`${env.SECRET_VALUE}`);
  },
} satisfies ExportedHandler<Env>;

なお現在設定しているシークレットの値はブラウザからは確認できませんが、

スクリーンショット 2025-05-06 195749

以下のコマンドで確認できます。

$ npx wrangler secret list
[
  {
    "name": "SECRET_VALUE",
    "type": "secret_text"
  }
]

Cloudflare サービスとの連携

いくつか他のCloudflare サービスとの連携方法を紹介します。

なお設定ファイル(wrangler.jsonc)にて度々bindingという設定が出てきますが、
これは単なる参照名の設定だけでなく、
Cloudflare Workersへ対象リソースへのアクセス権限付与も行っています。

https://developers.cloudflare.com/workers/runtime-apis/bindings/

R2

https://developers.cloudflare.com/r2/api/workers/workers-api-usage/

まず以下のコマンドでR2を作成して、

$ npx wrangler r2 bucket create test-bucket

wrangler.jsonc で以下の設定を追加します。

wrangler.jsonc
	  "r2_buckets": [
		{ "binding": "TEST_BUCKET", "bucket_name": "test-bucket" }
	  ],

Cloudflare Workers側では以下のようにR2を参照できます。

app.ts
export interface Env {
  TEST_BUCKET: R2Bucket;
}

export default {
  async fetch(request, env) {
    await env.TEST_BUCKET.put("hello.txt", "Hello, R2!");
    const object = await env.TEST_BUCKET.get("hello.txt");
    return new Response(object?.body);
  }
};

ファイル保存後、以下のように正常にR2にアクセスできるはずです。

$ curl http://localhost:5173/
Hello, R2!

KV

https://developers.cloudflare.com/kv/

まず以下のコマンドでR2を作成して、

$ npx wrangler kv namespace create test-kv

wrangler.jsonc で以下の設定を追加します。

wrangler.jsonc
	  "kv_namespaces": [
		{ "binding": "TEST_KV",
		  "id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
		}
	  ],
app.ts
export interface Env {
  TEST_KV: KVNamespace;
}

export default {
  async fetch(request, env) {
    await env.TEST_KV.put("foo", "bar");
    const value = await env.TEST_KV.get("foo");
    return new Response(`KV value: ${value}`);
  }
};

ファイル保存後、以下のように正常にKVにアクセスできるはずです。

$ curl http://localhost:5173/
KV value: bar

D1

https://developers.cloudflare.com/d1/worker-api/

まず以下のコマンドでD1を作成して、

$ npx wrangler d1 create test-database
wrangler.jsonc
	  "d1_databases": [
		{
		  "binding": "DB",
		  "database_name": "test-database",
		  "database_id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
		}
	  ],

セットアップ用のSQLを実行します。

$ npx wrangler d1 execute test-database --file setup.sql
setup.sql
DROP TABLE IF EXISTS users;

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL
);

INSERT INTO users (name) VALUES ('Alice');
INSERT INTO users (name) VALUES ('Bob');
INSERT INTO users (name) VALUES ('Charlie');

wrangler.jsonc で以下の設定を追加します。

app.ts
export interface Env {
  DB: D1Database;
}

export default {
  async fetch(request, env) {
    const result = await env.DB.prepare("SELECT * FROM users").all();
    return new Response(JSON.stringify(result.results), {
      headers: { "Content-Type": "application/json" },
    });
  }
};

ファイル保存後、以下のように正常にD1にアクセスできるはずです。

$ curl http://localhost:5173/
[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Charlie"}]

GitHub Actions での自動デプロイ

https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/

(1). アクセストークン取得

まずCloudflareでAPIトークンを取得します。
本記事ではAPI トークン テンプレートの「Cloudflare Workers を編集する」を利用しています。

スクリーンショット 2025-05-06 213030

(2). リポジトリ設定

次にリポジトリ側で以下のsecretsを設定します。

  • CLOUDFLARE_ACCOUNT_ID - CloudflareのアカウントID
  • CLOUDFLARE_API_TOKEN - 取得したAPIトークン

スクリーンショット 2025-05-06 212931

(3). ワークフロー ファイル作成

最後にワークフローファイルを作成します。
これをCommitしてあげれば、以降のPushからGitHub Actionsが動きます。

.github/workflows/deploy.yml
name: Deploy Worker
on:
  push:
    branches:
      - main
jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
      - name: Install
        run: npm install
      - name: Build & Deploy Worker
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

さいごに

以上、Cloudflare Workers + react router v7 でサクサクWebアプリを作る方法でした。

Cloudflare Workers の エッジ実行と React Router v7 の 最新ルーティング & データロードを組み合わせることで、SPA から SSR まで サクサクアプリが構築できます。
まずは無料枠で Hello World をデプロイし、今回紹介した 独自ドメイン・その他Cloudflareサービスとの連携・CI/CD などを少しずつ足していけば、
いつの間にかある程度のアプリが完成しています。

もし「本格的なサーバーやコンテナはコスト的にも使いたくないけど、手軽にアプリを作りたい…。」という人がいたら、Cloudflare Workers + React Router v7 の構成はとてもおすすめです。

小さく始めて、軽くデプロイして、世界中で一瞬で動く。
そんな体験がここにはあります。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.