1章_ローカル環境セットアップ
やること
cf-nextjs-sampleディレクトリ構成作成(monorepo)wranglerでローカル開発環境を作成
1. 環境確認(MAC)
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.02. pnpmを有効化
Corepackでpnpmを有効化する
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.0TIP
【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/webとapp/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リポジトリの初期化
すでに作成済みであれば不要。
git init.gitignoreも作成しておく
# =========================
# 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ディレクトリ作成
mkdir -p apps3-3. ルート package.json 作成
直接package.jsonを作成しても良いし、pnpm initで自動出力してから編集しても良い。
{
"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:apiとpnpm run dev:webを並行起動しつつ、Ctrl+Cで両方をまとめて停止できる。
【応用】より確実にプロセスを終了させたい場合は、以下のようにtrapを使う方法もある(上級者向け):
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に宣言することができる。
packages:
- "apps/*"3-5. Wrangler 導入
wranglerは両方のappsでpnpm exec wranglerとして利用する共通のCLIなので、workspaceルートに一度だけインストールするのが推奨です。
pnpm add -D wrangler -w
pnpm exec wrangler -vなお、appsごとに個別にwranglerを入れるとバージョンのズレが起きやすいため避けてください。
4. apps/web(Next.js)を作成
4-1. Next.js アプリ作成
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/が直下にある場合は、appをsrc/appに移動し、tsconfig.jsonのpathsを./src/*に変更する
4-2. monorepo を壊す“余計なファイル”を削除
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. ルートで依存解決
pnpm install4-4. apps/web/package.json の名前を統一
{
"name": "@cf-nextjs-sample/web"
}4-5. OpenNext 導入と設定
pnpm add -D @opennextjs/cloudflare --filter ./apps/webTIP
OpenNextとは
- 通常のNext.jsビルド(
next build)の成果物は、Node.js/Vercel前提で、Cloudflare Workersではそのまま動かない - そこで、OnepNext(
opennextjs-cloudflare build)を実行することで、
- 内部でnext build --webpackを実行
- Next.jsの成果物を解析
- Cloudflare Workers用に組み替える
- .open-next/worker.jsを生成
- この .open-next/worker.jsが後述の
wrangler.jsoncの"main": ".open-next/worker.js"で指定されてWeb Workerとしてデプロイされる
apps/web/package.jsonのscriptsに以下を追加:
"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 作成
{
"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. ディレクトリ作成
mkdir -p apps/api/src5-2. apps/api/package.json 作成
{
"name": "@cf-nextjs-sample/api",
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm exec wrangler dev",
"deploy": "pnpm exec wrangler deploy"
}
}5-3. 依存導入(ルートから)
pnpm --filter ./apps/api add hono
pnpm --filter ./apps/api add -D typescript @cloudflare/workers-types5-4. apps/api/tsconfig.json 作成
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"strict": true,
"types": ["@cloudflare/workers-types"]
},
"include": ["src"]
}5-5. apps/api/wrangler.jsonc 作成
{
"name": "cf-nextjs-sample-api-noimasaki",
"main": "src/index.ts",
"compatibility_date": "2026-01-18",
}5-6. apps/api/src/index.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 作成
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 を改修
"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. 依存を一括インストール(ルートで)
pnpm install7-2. 並行起動
pnpm run devdevスクリプトはdev:apiとdev: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.jsonのnameが合っていない可能性あり - → 例:
pnpm --filter ./apps/api add honoのように相対パス指定するか、package.jsonの名前を確認する
- →
/_next/static/* 404エラー- →
apps/web/wrangler.jsoncのassets設定が不足している可能性あり - →
.open-next/assetsのディレクトリとバインディングを正しく設定すること
- →
Node.jsの組み込みモジュール解決エラー
- →
compatibility_flags: ["nodejs_compat"]をwrangler.jsoncに追加する必要がある
- →
apps/web/pnpm-lock.yamlが再生成される- →
pnpm installをapps/web配下で実行している可能性 - → workspaceのルート(
cf-nextjs-sample)で必ず実行し、pnpm-workspace.yamlが存在することを確認する
- →
ここまでで、ローカルで apps/api と apps/web を monorepo で構築し、OpenNext + Wrangler を使って BFF 形式で API を呼び出し、wrangler dev で同時起動・動作確認ができる状態となりました。