はじめに
最近よく Cloudflare を使っています。使いやすさ、無料枠の大きさにとても感動しています。 (こちらのブログも Cloudflare Pages で配信しています)
また、普段の仕事ではよく Next.js を利用しており、Next.js on Cloudflare Workers が個人開発においては 今のところ最強なんじゃないかと思っています。
ということで、Cloudflare Workers + Next.js + D1 (とdrizzle)で開発する際の基本的な流れを紹介していきます。
今回紹介するサンプルコードの全量は以下にあります。
テンプレートの準備
以下のコマンドを実行し、テンプレートを作成します。 wrangler は適当にインストールしておいてください。
wrangler init my-app
カテゴリに Framework Starter を選択し、フレームワークとして Next.js を選択します。
コマンド実行結果
⛅️ wrangler 4.56.0 (update available 4.58.0)
─────────────────────────────────────────────
🌀 Running `npm create cloudflare@^2.5.0 my-app --`...
Need to install the following packages:
[email protected]
Ok to proceed? (y) y
> npx
> "create-cloudflare" my-app
──────────────────────────────────────────────────────────────────────────────────────────────────────────
👋 Welcome to create-cloudflare v2.62.1!
🧡 Let's get started.
📊 Cloudflare collects telemetry about your usage of Create-Cloudflare.
Learn more at: https://github.com/cloudflare/workers-sdk/blob/main/packages/create-cloudflare/telemetry.md
──────────────────────────────────────────────────────────────────────────────────────────────────────────
╭ Create an application with Cloudflare Step 1 of 3
│
├ In which directory do you want to create your application?
│ dir ./my-app
│
├ What would you like to start with?
│ category Framework Starter
│
├ Which development framework do you want to use?
│ framework Next.js
│
├ Cloning template from: https://github.com/opennextjs/opennextjs-cloudflare/tree/main/create-cloudflare/next
│
├ Updating name in `package.json`
│ updated `package.json`
│
├ Installing dependencies
│ installed via `npm install`
│
╰ Application created
╭ Configuring your application for Cloudflare Step 2 of 3
│
├ Installing wrangler A command line tool for building Cloudflare Workers
│ installed via `npm install wrangler --save-dev`
│
├ Retrieving current workerd compatibility date
│ compatibility date Could not find workerd date, falling back to 2025-09-27
│
├ Adding Wrangler files to the .gitignore file
│ updated .gitignore file
│
├ Generating types for your application
│ generated to `./cloudflare-env.d.ts` via `npm run cf-typegen`
│
├ Installing @types/node
│ installed via npm
│
├ Do you want to use git for version control?
│ no git
│
╰ Application configured
╭ Deploy with Cloudflare Step 3 of 3
│
├ Do you want to deploy your application?
│ no deploy via `npm run deploy`
│
╰ Done
────────────────────────────────────────────────────────────
🎉 SUCCESS Application created successfully!
💻 Continue Developing
Change directories: cd my-app
Deploy: npm run deploy
📖 Explore Documentation
https://developers.cloudflare.com/workers
🐛 Report an Issue
https://github.com/cloudflare/workers-sdk/issues/new/choose
💬 Join our Community
https://discord.cloudflare.com
────────────────────────────────────────────────────────────
DBマイグレーション(ローカル)
次に、ローカルDBに対してDBマイグレーションを行います。 今回は drizzle を使ってみます。
npm install drizzle-orm
npm install -D drizzle-kit
package.json の scripts に以下を加えてください。
"drizzle:generate": "drizzle-kit generate --config=./drizzle.config.ts"
drizzle.config.ts と drizzle/schema.ts を用意します。drizzle ディレクトリはあらかじめ作成しておいてください。
import type { Config } from "drizzle-kit";
export default {
schema: "./drizzle/schema.ts",
out: "./drizzle/migrations",
dialect: "sqlite",
driver: "d1-http",
} satisfies Config;
drizzle.config.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { sql } from "drizzle-orm";
export const todos = sqliteTable("todos", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
completed: integer("completed", { mode: "boolean" }).notNull().default(false),
createdAt: integer("created_at")
.notNull()
.default(sql`(unixepoch())`),
});
drizzle/schema.ts
drizzle によってマイグレーションファイルを作成します。
npm run drizzle:generate
以下のようになれば成功です。
> [email protected] drizzle:generate
> drizzle-kit generate --config=./drizzle.config.ts
Reading config file '/Users/a2-ito/work/private/work/20260112/my-app/drizzle.config.ts'
1 tables
todos 4 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/migrations/0000_milky_golden_guardian.sql 🚀
マイグレーションファイルが作成できたので、実際にローカルDBに適用していきます。 ローカル開発の段階では、Cloudflare 上に DB を作成する必要はありません。 ただし、以下の設定を wrangler 設定ファイルに追記しておく必要があります。
"d1_databases": [
{
"binding": "DB",
"database_name": "my-app-db",
"database_id": "my-app-db",
"migrations_dir": "drizzle/migrations",
}
]
wrangler.jsonc
マイグレーションを実行します。
npx wrangler d1 migrations apply my-app-db --local
以下のようになればマイグレーション成功です。
⛅️ wrangler 4.58.0
───────────────────
Resource location: local
Use --remote if you want to access the remote instance.
Migrations to be applied:
┌────────────────────────────────┐
│ name │
├────────────────────────────────┤
│ 0000_milky_golden_guardian.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 my-app-db (my-app-db) 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_milky_golden_guardian.sql │ ✅ │
└────────────────────────────────┴────────┘
テーブルが作成できているかどうか、見てみましょう。
npx wrangler d1 execute my-app-db --local --command "PRAGMA table_info(todos);"
以下のように表示されればOKです。
⛅️ wrangler 4.58.0
───────────────────
Resource location: local
Use --remote if you want to access the remote instance.
🌀 Executing on local database my-app-db (my-app-db) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 1 command executed successfully.
┌─────┬────────────┬─────────┬─────────┬─────────────┬────┐
│ cid │ name │ type │ notnull │ dflt_value │ pk │
├─────┼────────────┼─────────┼─────────┼─────────────┼────┤
│ 0 │ id │ INTEGER │ 1 │ null │ 1 │
├─────┼────────────┼─────────┼─────────┼─────────────┼────┤
│ 1 │ title │ TEXT │ 1 │ null │ 0 │
├─────┼────────────┼─────────┼─────────┼─────────────┼────┤
│ 2 │ completed │ INTEGER │ 1 │ false │ 0 │
├─────┼────────────┼─────────┼─────────┼─────────────┼────┤
│ 3 │ created_at │ INTEGER │ 1 │ unixepoch() │ 0 │
└─────┴────────────┴─────────┴─────────┴─────────────┴────┘
テストアプリでのDBアクセス
API経由でデータを取得してみます。
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { NextRequest, NextResponse } from "next/server";
import { todos } from "../../../../drizzle/schema";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
export async function GET(_req: NextRequest) {
const { env } = getCloudflareContext();
const db = drizzle(env.DB);
const result = await db.select().from(todos);
return NextResponse.json(result);
}
src/app/api/todos/route.ts
declare namespace Cloudflare {
interface Env {
NEXTJS_ENV: string;
WORKER_SELF_REFERENCE: Fetcher;
IMAGES: ImagesBinding;
ASSETS: Fetcher;
DB: D1Database; /* これを追加 */
}
}
cloudflare-env.d.ts
データを入れる前に動作確認しておきましょう。
% curl localhost:3000/api/todos
空データが返却されればOKです。
[]%
作成済みのテーブルにデータを入れてみましょう。 今回のサンプルアプリでは GET API しか作らないので、DBに直接 insert します。
npx wrangler d1 execute my-app-db \
--local \
--command "INSERT INTO todos (title) VALUES ('タスクA'), ('タスクB');"
こんな感じで値が入っていたらOKです。
npx wrangler d1 execute my-app-db \
--local \
--command "select * from todos;"
⛅️ wrangler 4.58.0 (update available 4.59.2)
─────────────────────────────────────────────
Resource location: local
Use --remote if you want to access the remote instance.
🌀 Executing on local database my-app-db (my-app-db) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 1 command executed successfully.
┌────┬─────────┬───────────┬────────────┐
│ id │ title │ completed │ created_at │
├────┼─────────┼───────────┼────────────┤
│ 1 │ タスクA │ 0 │ 1768616416 │
├────┼─────────┼───────────┼────────────┤
│ 2 │ タスクB │ 0 │ 1768616426 │
└────┴─────────┴───────────┴────────────┘
再度APIをコールします。
% curl localhost:3000/api/todos
以下のように値が返ってくれば完成です!
[{"id":1,"title":"タスクA","completed":false,"createdAt":1768616416},{"id":2,"title":"タスクB","completed":false,"createdAt":1768616426}]%
Cloudflare Workers 上で動作確認
まずは D1 インスタンスを作成します。
npx wrangler d1 create my-app-db
⛅️ wrangler 4.58.0 (update available 4.59.2)
─────────────────────────────────────────────
✅ Successfully created DB 'my-app-db' in region APAC
Created your new D1 database.
To access your new D1 Database in your Worker, add the following snippet to your configuration file:
{
"d1_databases": [
{
"binding": "my_app_db",
"database_name": "my-app-db",
"database_id": "b0c1b30a-f9b4-418c-b9df-2d1d1036301f"
}
]
}
✔ Would you like Wrangler to add it on your behalf? … yes
✔ What binding name would you like to use? … my_app_db
✔ For local dev, do you want to connect to the remote resource instead of a local resource? … no
プロンプトで確認される質問について確認しておきます。
✔ Would you like Wrangler to add it on your behalf? … yes
wrangler 設定ファイル(ここでは wrangler.jsonc)に設定を追加するか?と聞かれています。 Cloudflare Workers 環境で動く際にはデータベースIDの設定が必要となるので、追加してもらいましょう。
✔ What binding name would you like to use? … my_app_db
DBにアクセスする際に使用するバインド名を確認されています。 あとで設定ファイル上で修正するので一旦デフォルトのままでよいです。
✔ For local dev, do you want to connect to the remote resource instead of a local resource? … no
ローカル開発時に、デフォルトでリモートDBに接続するか、聞かれています。
好みですが、ローカル開発はローカルDBに接続する方が良いでしょう。
なお、remote: true を加えれば後からこの設定を付与できます。
wrangler.jsonc が以下のように更新されています。
"d1_databases": [
{
"binding": "DB",
"database_name": "my-app-db",
"database_id": "my-app-db",
"migrations_dir": "drizzle/migrations",
},
{
"binding": "my_app_db",
"database_name": "my-app-db",
"database_id": "b0c1b30a-f9b4-418c-b9df-2d1d1036301f"
}
]
wrangler.jsonc
以下のようにマージしましょう。
"d1_databases": [
{
"binding": "DB",
"database_name": "my-app-db",
"migrations_dir": "drizzle/migrations",
"database_id": "b0c1b30a-f9b4-418c-b9df-2d1d1036301f"
}
]
改めて、D1インスタンスが作成されていることを確認しておきます。
npx wrangler d1 list
⛅️ wrangler 4.58.0 (update available 4.59.2)
─────────────────────────────────────────────
┌──────────────────────────────────────┬─────────────────┬──────────────────────────┬────────────┬────────────┬───────────┬──────────────┐
│ uuid │ name │ created_at │ version │ num_tables │ file_size │ jurisdiction │
├──────────────────────────────────────┼─────────────────┼──────────────────────────┼────────────┼────────────┼───────────┼──────────────┤
│ b0c1b30a-f9b4-418c-b9df-2d1d1036301f │ my-app-db │ 2026-01-17T02:28:20.388Z │ production │ 0 │ 12288 │ │
└──────────────────────────────────────┴─────────────────┴──────────────────────────┴────────────┴────────────┴───────────┴──────────────┘
サンプルアプリを Cloudflare 環境にデプロイします。
npm run deploy
コマンド実行結果
> [email protected] deploy
> opennextjs-cloudflare build && opennextjs-cloudflare deploy
┌─────────────────────────────┐
│ OpenNext — Cloudflare build │
└─────────────────────────────┘
App directory: /Users/a2-ito/work/private/work/20260112/my-app
Next.js version : 15.5.9
@opennextjs/cloudflare version: 1.14.8
@opennextjs/aws version: 3.9.7
┌─────────────────────────────────┐
│ OpenNext — Building Next.js app │
└─────────────────────────────────┘
> [email protected] build
> next build
Using vars defined in .dev.vars
▲ Next.js 15.5.9
Creating an optimized production build ...
Using vars defined in .dev.vars
Using vars defined in .dev.vars
Using vars defined in .dev.vars
✓ Compiled successfully in 2.1s
./src/app/api/todos/route.ts
5:10 Warning: 'eq' is defined but never used. @typescript-eslint/no-unused-vars
8:27 Warning: '_req' is defined but never used. @typescript-eslint/no-unused-vars
info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/app/api-reference/config/eslint#disabling-rules
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (5/5)
✓ Collecting build traces
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 5.44 kB 107 kB
├ ○ /_not-found 994 B 103 kB
└ ƒ /api/todos 123 B 102 kB
+ First Load JS shared by all 102 kB
├ chunks/255-cb395327542b56ef.js 45.9 kB
├ chunks/4bd1b696-c023c6e3521b1417.js 54.2 kB
└ other shared chunks (total) 1.9 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
┌──────────────────────────────┐
│ OpenNext — Generating bundle │
└──────────────────────────────┘
Bundling middleware function...
Bundling static assets...
Bundling cache assets...
Building server function: default...
Applying code patches: 1.672s
# copyPackageTemplateFiles
⚙️ Bundling the OpenNext server...
Worker saved in `.open-next/worker.js` 🚀
OpenNext build complete.
┌──────────────────────────────┐
│ OpenNext — Cloudflare deploy │
└──────────────────────────────┘
Using vars defined in .dev.vars
Incremental cache does not need populating
Tag cache does not need populating
⛅️ wrangler 4.58.0 (update available 4.59.2)
─────────────────────────────────────────────
🌀 Building list of assets...
✨ Read 41 files from the assets directory /Users/a2-ito/work/private/work/20260112/my-app/.open-next/assets
🌀 Starting asset upload...
🌀 Found 29 new or modified static assets to upload. Proceeding with upload...
+ /BUILD_ID
+ /_next/static/chunks/app/page-a9ed04b466e51d7a.js
+ /window.svg
+ /_next/static/chunks/main-app-9d9d526d3a226d6b.js
+ /next.svg
+ /_next/static/chunks/webpack-b66e35ffe30362a3.js
+ /_next/static/chunks/356-e5489542cb0f1b9a.js
+ /_next/static/media/ba015fad6dcf6784-s.woff2
+ /_next/static/chunks/polyfills-42372ed130431b0a.js
+ /_next/static/chunks/4bd1b696-c023c6e3521b1417.js
+ /_next/static/W-TPjPGxCIT4REd3OTDrm/_ssgManifest.js
+ /_next/static/chunks/pages/_error-cb2a52f75f2162e2.js
+ /file.svg
+ /_next/static/chunks/app/layout-0d13b2c9f228edd5.js
+ /favicon.svg
+ /_next/static/media/9610d9e46709d722-s.woff2
+ /_next/static/css/f5bd69e6be144534.css
+ /_next/static/media/4cf2300e9c8272f7-s.p.woff2
+ /_next/static/chunks/main-a4753544b5b73813.js
+ /_next/static/chunks/framework-956314ae4e2456c3.js
+ /_next/static/chunks/app/api/todos/route-3778ef6b3c7e2ea6.js
+ /_next/static/chunks/pages/_app-7d307437aca18ad4.js
+ /_next/static/W-TPjPGxCIT4REd3OTDrm/_buildManifest.js
+ /globe.svg
+ /_next/static/chunks/app/_not-found/page-ec6ae770db98bae0.js
+ /_next/static/media/747892c23ea88013-s.woff2
+ /_next/static/media/8d697b304b401681-s.woff2
+ /_next/static/media/93f479601ee12b01-s.p.woff2
+ /_next/static/chunks/255-cb395327542b56ef.js
Uploaded 9 of 29 assets
Uploaded 19 of 29 assets
Uploaded 29 of 29 assets
✨ Success! Uploaded 29 files (2.62 sec)
Total Upload: 5035.08 KiB / gzip: 1034.42 KiB
Worker Startup Time: 29 ms
Your Worker has access to the following bindings:
Binding Resource
env.DB (my-app-db) D1 Database
env.WORKER_SELF_REFERENCE (my-app) Worker
env.IMAGES Images
env.ASSETS Assets
Uploaded my-app (11.55 sec)
Deployed my-app triggers (1.25 sec)
https://my-app.xxx.workers.dev
Current Version ID: 93da5c06-c95b-4acf-bad2-350b420bdf6c
(node:29933) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
デプロイが正常終了すると、
https://my-app.xxx.workers.dev
のようなアプリ用のドメインが発行されるので、メモしておきましょう。
この段階ではまだデータが入っていないので、リモートDBにデータを登録します。
マイグレーションしてから、データを insert していきます。
npx wrangler d1 migrations apply my-app-db --remote
npx wrangler d1 execute my-app-db \
--remote \
--command "INSERT INTO todos (title) VALUES ('タスクA'), ('タスクB');"
npx wrangler d1 execute my-app-db \
--remote \
--command "select * from todos;"
データが入っていることが確認できたら、ローカル同様に curl で API を叩きます。 こちらで同じように値が返却されれば完成です。
curl https://my-app.xxx.workers.dev/api/todos
後片付け
アプリの削除
npx wrangler delete my-app-db
データベースの削除
npx wrangler d1 delete my-app-db
おわりに
Cloudflare Workers + Next.js + D1 + drizzle 構成でのローカル開発、サーバデプロイまでの最小ステップを紹介しました。 個人開発のような小規模のアプリではかなり体験が良く、かつコスパ良く開発できる環境ではないかと思います。