Next.js on Cloudflare Workers を試す (open-next)

前にこれやって一通り動かせるけど動きがあんまり安定してなくて使いづらかった
next-on-pages から open-next ベースになって、良い評判が聞こえてくるようになったので改めて試す

ボイラープレートを作る
$ pnpm create cloudflare@latest nextjs-cloudflare-workers --framework=next

コマンド叩くだけでデプロイまでやってくれた

package.json:
{
"name": "nextjs-cloudflare-workers",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"cf-typegen": "wrangler types --env-interface CloudflareBindings ./cloudflare-env.d.ts"
},
"dependencies": {
"@opennextjs/cloudflare": "^1.0.2",
"next": "15.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20.17.47",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"tailwindcss": "^4",
"typescript": "^5",
"wrangler": "^4.15.2"
}
}
wrangler pages dev ではなく pnpm dev が dev script に登録されている
next-on-pages のときも experimental で next dev で Cloudflare Bindings を利用することができたけど、open-next でも使えるらしい。
next.config.ts を見るとそれっぽい設定が追加されている:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
// added by create cloudflare to enable calling `getCloudflareContext()` in `next dev`
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
initOpenNextCloudflareForDev();

pnpm dev してみると
➜ pnpm dev
> nextjs-cloudflare-workers@0.1.0 dev /path/to/nextjs-cloudflare-workers
> next dev --turbopack
Using vars defined in .dev.vars
▲ Next.js 15.3.2 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.12:3000
✓ Starting...
Using vars defined in .dev.vars
Using vars defined in .dev.vars
Using vars defined in .dev.vars
✓ Ready in 915ms
Using vars defined in .dev.vars
Using vars defined .dev.vars が重複して呼ばれてそうだけどこのログがあるので initOpenNextCloudflareForDev による処理が動いてるっぽくみえる

d1 を追加していく
➜ pnpm wrangler d1 create nextjs-cloudflare-workers-db
⛅️ wrangler 4.15.2
-------------------
✅ Successfully created DB 'nextjs-cloudflare-workers-db' in region APAC
Created your new D1 database.
{
"d1_databases": [
{
"binding": "DB",
"database_name": "nextjs-cloudflare-workers-db",
"database_id": "xxx"
}
]
}
作成できた
表示された json を wrangler.jsonc に貼り付ける
$ git diff wrangler.jsonc
diff --git a/wrangler.jsonc b/wrangler.jsonc
index 8243c85..77e7516 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -31,6 +31,14 @@
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/
+ "d1_databases": [
+ {
+ "binding": "DB",
+ "database_name": "nextjs-cloudflare-workers-db",
+ "database_id": "xxx"
+ }
+ ],
+
/**
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
追加したら型定義を更新しておく。
$ pnpm cf-typegen
> nextjs-cloudflare-workers@0.1.0 cf-typegen /path/to/nextjs-cloudflare-workers
> wrangler types --env-interface CloudflareBindings ./cloudflare-env.d.ts
⛅️ wrangler 4.15.2
-------------------
Generating project types...
declare namespace Cloudflare {
interface Env {
NEXTJS_ENV: string;
DB: D1Database;
ASSETS: Fetcher;
}
}
interface CloudflareBindings extends Cloudflare.Env {}
Generating runtime types...
Runtime types generated.
────────────────────────────────────────────────────────────
✨ Types written to ./cloudflare-env.d.ts
📖 Read about runtime types
https://developers.cloudflare.com/workers/languages/typescript/#generate-types
📣 Remember to rerun 'wrangler types' after you change your wrangler.json file.

DB にアクセスするのに drizzle を使う
がっと必要な設定と依存を追加する
$ pnpm add drizzle-orm
$ pnpm add -D drizzle-kit
import { defineConfig } from "drizzle-kit";
const requiredEnv = (key: string) => {
const value = process.env[key];
if (value === undefined) {
throw new Error(`Environment variable ${key} is not set.`);
}
return value;
};
export default defineConfig({
out: "./drizzle/migrations",
schema: "./src/db/schema.ts",
dialect: "sqlite",
driver: "d1-http",
dbCredentials: {
accountId: requiredEnv("CLOUDFLARE_ACCOUNT_ID"),
databaseId: requiredEnv("CLOUDFLARE_DATABASE_ID"),
token: requiredEnv("CLOUDFLARE_D1_TOKEN"),
},
});
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const usersTable = sqliteTable("users", {
id: int().primaryKey({ autoIncrement: true }),
email: text().notNull(),
});
CLOUDFLARE_ACCOUNT_ID=FIXME
CLOUDFLARE_DATABASE_ID=FIXME
CLOUDFLARE_D1_TOKEN=FIXME
diff --git a/package.json b/package.json
index dc426af..850d960 100644
--- a/package.json
+++ b/package.json
@@ -12,12 +12,16 @@
"fix": "run-s 'fix:*'",
"fix:biome-format": "biome format --write .",
"fix:biome-lint": "biome check --write .",
+ "migrate:generate": "drizzle-kit generate",
+ "migrate:local": "wrangler d1 migrations apply nextjs-cloudflare-workers-db --local",
+ "migrate:prod": "wrangler d1 migrations apply nextjs-cloudflare-workers-db --remote",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
- "cf-typegen": "wrangler types --env-interface CloudflareBindings ./cloudflare-env.d.ts"
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts"
},
"dependencies": {
diff --git a/wrangler.jsonc b/wrangler.jsonc
index 92f9e85..58a6df2 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -32,7 +32,8 @@
{
"binding": "DB",
"database_name": "nextjs-cloudflare-workers-db",
- "database_id": "xxx"
+ "database_id": "xxx",
+ "migrations_dir": "drizzle/migrations"
}
]
マイグレーションを作成する
$ pnpm migrate:generate
> nextjs-cloudflare-workers@0.1.0 migrate:generate /path/to/nextjs-cloudflare-workers
> drizzle-kit generate
No config path provided, using default 'drizzle.config.ts'
Reading config file '/Users/kaito/Playground/nextjs-cloudflare-workers/drizzle.config.ts'
1 tables
users 2 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/migrations/0000_absurd_the_renegades.sql 🚀
そのままローカルDBにマイグレーションを適用する
$ pnpm migrate:local
> nextjs-cloudflare-workers@0.1.0 migrate:local /path/to/nextjs-cloudflare-workers
> wrangler d1 migrations apply nextjs-cloudflare-workers-db --local
⛅️ wrangler 4.15.2
-------------------
Migrations to be applied:
┌───────────────────────────────┐
│ name │
├───────────────────────────────┤
│ 0000_absurd_the_renegades.sql │
└───────────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on local database nextjs-cloudflare-workers-db (xxx) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 2 commands executed successfully.
┌───────────────────────────────┬────────┐
│ name │ status │
├───────────────────────────────┼────────┤
│ 0000_absurd_the_renegades.sql │ ✅ │
└───────────────────────────────┴────────┘
マイグレーションがあたった。
最後にOpenNext の d1-example を参考にDBクライアントインスタンスを用意する
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { drizzle } from "drizzle-orm/d1";
import { cache } from "react";
import * as schema from "./schema";
export const getDb = cache(() => {
const { env } = getCloudflareContext<{
DB: D1Database;
}>();
return drizzle(env.DB, { schema });
});
これで一通りの準備が整った

適当に server actions で呼び出してみる
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e68abe6..3eecd64 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,6 +1,15 @@
+import { getDb } from "@/db/client";
import Image from "next/image";
-export default function Home() {
+export default async function Home() {
+ async function debug() {
+ "use server";
+ const db = getDb();
+
+ const users = await db.query.usersTable.findMany();
+ console.log("users", users);
+ }
+
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-s
creen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
@@ -25,6 +34,10 @@ export default function Home() {
</li>
</ol>
+ <form action={debug}>
+ <button>Debug Db Call</button>
+ </form>
+
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors
flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark
:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
pnpm dev で開発サーバーを起動して
❯ pnpm dev
> nextjs-cloudflare-workers@0.1.0 dev /path/to/nextjs-cloudflare-workers
> next dev --turbopack
Using vars defined in .dev.vars
▲ Next.js 15.3.2 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.12:3000
- Environments: .env
✓ Starting...
Using vars defined in .dev.vars
Using vars defined in .dev.vars
Using vars defined in .dev.vars
✓ Ready in 1125ms
Using vars defined in .dev.vars
○ Compiling / ...
✓ Compiled / in 1135ms
GET / 200 in 1286ms
users []
POST / 200 in 77ms
server actions を介して正常に users を取得できていることがわかる 🤟

軽く触ってみたけど、ネックだったローカル開発が通常の Next.js とほぼ変わらなくて良さげだった

いつの間にか process.env も使えるようになっていてかなり使いやすくなってる