Skip to content

1章_ローカル環境セットアップ

やること

  • cf-nextjs-sample ディレクトリ構成作成(monorepo)
  • wranglerでローカル開発環境を作成

1. 環境確認(MAC)

bash
noimasaki@MacBook-Air cf-nextjs-sample % node -v
v23.10.0
noimasaki@MacBook-Air cf-nextjs-sample % corepack --version
0.32.0
noimasaki@MacBook-Air cf-nextjs-sample % git --version
git version 2.49.0

2. pnpmを有効化

Corepackでpnpmを有効化する

bash
noimasaki@MacBook-Air cf-nextjs-sample % corepack enable
noimasaki@MacBook-Air cf-nextjs-sample % corepack prepare pnpm@latest --activate
Preparing pnpm@latest for immediate activation...
noimasaki@MacBook-Air cf-nextjs-sample % pnpm -v
10.28.0

TIP

【corepackとは】

  • corepack は「パッケージマネージャ管理用のランタイム付属ツール」
  • Node.js に同梱されており、pnpm / yarn などを “正しいバージョンで” 使うための仕組み
  • pnpm そのものではなく、pnpm を起動・管理するためのラッパー

package.json"packageManager": "pnpm@x.x.x"を参照して利用するpnpmバージョンを判断する

TIP

【pnpmとは】 Node.js 単体では「JavaScriptを実行する」だけで、以下は全部外部ツールです。

  • 依存ライブラリの取得(react, hono など)
  • node_modules の管理
  • lockfile の管理
  • monorepo の管理 これを担当するのが、npm/yarn/pnpmなどのパッケージマネージャ

pnpmはNode標準ではないもの、今回のようなmonorepoと相性が良い。 というのも、pnpmは「依存を1箇所にまとめて、リンクで使い回す」 今回のようなトップディレクトリ直下のnode_modulesの実態をおき、 その配下のapp/webapp/apiに用意されるnode_modulesはリンクしか置かない構成をとることが可能で、 依存関係が壊れにくく、installも早く、ディスク効率も良い

TIP

【pnpm dlx】 Next.jsのプロジェクト作成時によく見るコマンドは npx create-next-app my-appであるが、今回利用したのは pnpm dlx create-next-app my-appであり、その違いについて。

実施していることはどちらも同じであるが、 「npxはnpm所属」で「pnpm dlxはpnpm所属」の違いがある。 今回はpnpmを使いたいのでプロジェクト作成はpnpm dlx

3. リポジトリ作成(pnpm workspace)

プロジェクトのトップディレクトリcf-nextjs-sampleは作成済みとする。

3-1. Gitリポジトリの初期化

すでに作成済みであれば不要。

bash
git init

.gitignoreも作成しておく

text
# =========================
# Dependencies
# =========================
node_modules/
**/node_modules/

# =========================
# Next.js
# =========================
.next/
**/.next/
out/
**/out/

# =========================
# OpenNext
# =========================
.open-next/
**/.open-next/

# =========================
# Cloudflare Wrangler
# =========================
.wrangler/
**/.wrangler/

# =========================
# Logs / Cache
# =========================
npm-debug.log*
pnpm-debug.log*
.yarn-debug.log*
.yarn-error.log*

# =========================
# OS / Editor
# =========================
.DS_Store
.vscode/
.idea/

3-2. appsディレクトリ作成

bash
mkdir -p apps

3-3. ルート package.json 作成

直接package.jsonを作成しても良いし、pnpm initで自動出力してから編集しても良い。

json
{
  "name": "cf-nextjs-sample",
  "private": true,
  "packageManager": "pnpm@10.28.0",
  "scripts": {
    "dev:api": "pnpm --filter ./apps/api exec wrangler dev --port 8788",
    "dev:web": "pnpm --filter ./apps/web run build:cf && pnpm --filter ./apps/web exec wrangler dev --port 8787",
    "dev": "bash -lc 'pnpm run dev:api & pnpm run dev:web & wait'"
  }
}

TIP

bash -lc の説明

  • -l は login shell を意味し、環境変数や設定ファイルを読み込むために使う
  • -c は引数の文字列をコマンドとして実行するためのオプション
  • & はコマンドをバックグラウンドで実行する
  • wait はbashがバックグラウンドジョブの終了を待つため、bashがフォアグラウンドに留まり、SIGINT(Ctrl+C)が親プロセスに届くようにする

これにより、pnpm run dev:apipnpm run dev:webを並行起動しつつ、Ctrl+Cで両方をまとめて停止できる。

【応用】より確実にプロセスを終了させたい場合は、以下のようにtrapを使う方法もある(上級者向け):

bash
bash -lc 'trap "kill 0" INT TERM; pnpm run dev:api & pnpm run dev:web & wait'

3-4. pnpm-workspace.yaml 作成

今回、monorepoとするため、pnpm-workspace.yamlをプロジェクトルートに作成する。これを作成することで、「"app/*"配下の複数プロジェクトを一つとして扱う」とpnpmに宣言することができる。

yaml
packages:
  - "apps/*"

3-5. Wrangler 導入

wranglerは両方のappsでpnpm exec wranglerとして利用する共通のCLIなので、workspaceルートに一度だけインストールするのが推奨です。

bash
pnpm add -D wrangler -w
pnpm exec wrangler -v

なお、appsごとに個別にwranglerを入れるとバージョンのズレが起きやすいため避けてください。


4. apps/web(Next.js)を作成

4-1. Next.js アプリ作成

bash
pnpm dlx create-next-app@latest apps/web

※質問でTypeScript、App Router、Tailwind CSSなどを選択してください(推奨)

TIP

src/app 構成にしたい場合

  • create-next-app の質問にある "src/ directory" を Yes にすると最初から src/app 構成になる
  • すでに app/ が直下にある場合は、appsrc/app に移動し、tsconfig.jsonpaths./src/* に変更する

4-2. monorepo を壊す“余計なファイル”を削除

bash
rm -f apps/web/pnpm-lock.yaml
rm -rf apps/web/node_modules
rm -f apps/web/pnpm-workspace.yaml
rm -f apps/web/.gitignore
rm -f apps/web/README.md
rm -rf apps/web/.next
rm -rf apps/web/.git
  • 理由:pnpmはルートでlockfileを管理し、workspaceとして一元管理するため、apps/web内のlockfileやnode_modulesは不要で重複・混乱の元になります。
  • .nextはビルド成果物なので、開発時に不要なら削除しても良いです。

4-3. ルートで依存解決

bash
pnpm install

4-4. apps/web/package.json の名前を統一

json
{
  "name": "@cf-nextjs-sample/web"
}

4-5. OpenNext 導入と設定

bash
pnpm add -D @opennextjs/cloudflare --filter ./apps/web

TIP

OpenNextとは

  • 通常のNext.jsビルド(next build)の成果物は、Node.js/Vercel前提で、Cloudflare Workersではそのまま動かない
  • そこで、OnepNext(opennextjs-cloudflare build)を実行することで、
  1. 内部でnext build --webpackを実行
  2. Next.jsの成果物を解析
  3. Cloudflare Workers用に組み替える
  4. .open-next/worker.jsを生成
  • この .open-next/worker.jsが後述のwrangler.jsonc"main": ".open-next/worker.js"で指定されてWeb Workerとしてデプロイされる

apps/web/package.jsonのscriptsに以下を追加:

json
"scripts": {
  "build": "next build --webpack",
  "build:cf": "opennextjs-cloudflare build"
}

TIP

next build --webpack の理由

  • Next.js 16のデフォルトビルダーがwebpackからTurbopackになった
  • OpenNextがバージョンによってはTurbopackにを前提にしていない
  • そこで、Next.jsビルド時にwebpackに明示的に指定することで、OpenNextが解釈可能となる

4-6. apps/web/wrangler.jsonc 作成

jsonc
{
  "name": "cf-nextjs-sample-web-noimasaki",
  "main": ".open-next/worker.js",
  "compatibility_date": "2026-01-18",
  "compatibility_flags": ["nodejs_compat"],

  // OpenNext が出力する静的アセットを配信する
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  },

  // Web Worker が API に辿り着くための Service Binding
  "services": [
    {
      "binding": "API",
      "service": "cf-nextjs-sample-api-noimasaki"
    }
  ]
}

5. apps/api(Cloudflare Workers + Hono)を作成

5-1. ディレクトリ作成

bash
mkdir -p apps/api/src

5-2. apps/api/package.json 作成

json
{
  "name": "@cf-nextjs-sample/api",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "pnpm exec wrangler dev",
    "deploy": "pnpm exec wrangler deploy"
  }
}

5-3. 依存導入(ルートから)

bash
pnpm --filter ./apps/api add hono
pnpm --filter ./apps/api add -D typescript @cloudflare/workers-types

5-4. apps/api/tsconfig.json 作成

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "Bundler",
    "strict": true,
    "types": ["@cloudflare/workers-types"]
  },
  "include": ["src"]
}

5-5. apps/api/wrangler.jsonc 作成

jsonc
{
  "name": "cf-nextjs-sample-api-noimasaki",
  "main": "src/index.ts",
  "compatibility_date": "2026-01-18",
}

5-6. apps/api/src/index.ts 作成

ts
import { Hono } from "hono";

const app = new Hono();

app.get("/health", (c) => c.json({ ok: true }));

app.get("/", (c) => c.text("cf-nextjs-sample-api"));

export default app;

6. Web を BFF 化して API を Service Binding で呼ぶ

6-1. apps/web/src/app/api/health/route.ts 作成

ts
import { getCloudflareContext } from "@opennextjs/cloudflare";

/**
 * Cloudflare Workers 上で動く Next.js(OpenNext)用の BFF エンドポイント。
 *
 * ブラウザは `/api/health`(= Web Worker)だけを呼ぶ。
 * Web Worker は Service Binding `env.API` 経由で API Worker の `/health` を呼ぶ。
 *
 * 重要:
 * - ローカル next dev では動かす前提にしない(Service Binding が無いため)
 * - 動作確認は `wrangler dev`(OpenNextで生成した worker)で行う
 */
export async function GET() {
  // OpenNext が Cloudflare の env を渡してくれる(Worker実行時のみ有効)
  const { env } = getCloudflareContext();

  // TypeScript の型には env.API が出ないので、最小限の型で扱う
  const api = (env as unknown as { API: { fetch: typeof fetch } }).API;

  // Service Binding ではホスト名は意味がないので "https://internal" を慣習的に使う
  const upstream = await api.fetch("https://internal/health", { cache: "no-store" });

  // upstream の JSON をそのまま返す(失敗時も status を引き継ぐ)
  return new Response(await upstream.text(), {
    status: upstream.status,
    headers: { "content-type": upstream.headers.get("content-type") ?? "application/json" },
  });
}

6-2. apps/web/src/app/page.tsx を改修

ts
"use client";

import { useState } from "react";

export default function Home() {
  const [result, setResult] = useState<string>("");

  const callApi = async () => {
    setResult("loading...");
    const res = await fetch("/api/health", { cache: "no-store" });
    const json = await res.json();
    setResult(JSON.stringify(json));
  };

  return (
    <main style={{ padding: 24 }}>
      <h1>cf-nextjs-sample</h1>
      <p>API: (via BFF /api/health)</p>

      <button onClick={callApi} style={{ padding: "8px 12px" }}>
        Call /health
      </button>

      <pre style={{ marginTop: 16 }}>{result}</pre>
    </main>
  );
}

TIP

ローカルでの動作確認は、wrangler dev(OpenNext Worker)で行い、next devではService BindingがないためBFFは動きません。


7. ローカルで Web + API を wrangler で同時起動して確認

7-1. 依存を一括インストール(ルートで)

bash
pnpm install

7-2. 並行起動

bash
pnpm run dev
  • dev スクリプトは dev:apidev:web を並行起動し、apiはポート8788、webはポート8787で起動します。

7-3. 動作確認

  • ブラウザで http://localhost:8787 にアクセス
  • 「Call /health」ボタンを押すと { "ok": true } が表示されることを確認
  • API Worker は http://localhost:8788/health にcurlなどで直接アクセスしても { "ok": true } が返ります

8. よくあるつまずき(この章の範囲)

  • No projects matched the filters エラー

    • --filter の指定が間違っているか、package.jsonnameが合っていない可能性あり
    • → 例: pnpm --filter ./apps/api add hono のように相対パス指定するか、package.jsonの名前を確認する
  • /_next/static/* 404 エラー

    • apps/web/wrangler.jsoncassets 設定が不足している可能性あり
    • .open-next/assets のディレクトリとバインディングを正しく設定すること
  • Node.jsの組み込みモジュール解決エラー

    • compatibility_flags: ["nodejs_compat"]wrangler.jsonc に追加する必要がある
  • apps/web/pnpm-lock.yaml が再生成される

    • pnpm installapps/web 配下で実行している可能性
    • → workspaceのルート(cf-nextjs-sample)で必ず実行し、pnpm-workspace.yaml が存在することを確認する

ここまでで、ローカルで apps/apiapps/web を monorepo で構築し、OpenNext + Wrangler を使って BFF 形式で API を呼び出し、wrangler dev で同時起動・動作確認ができる状態となりました。