Open10

Next.js on Cloudflare Workers を試す (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 を見るとそれっぽい設定が追加されている:

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
drizzle.config.ts
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"),
  },
});
src/db/schema.ts
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クライアントインスタンスを用意する

src/db/client.ts
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 を取得できていることがわかる 🤟