OpenNext + Cloudflare D1 を使った開発で、ローカル環境での開発・テスト・本番への移行を効率的に行いたいと考えたことはありませんか。
愚直に実現しようとすると、環境ごとに異なる設定ファイルを用意したり、テストデータの管理に手間がかかります。Drizzle のエコシステム(drizzle-orm、drizzle-kit、drizzle-seed)を活用することで、これらの課題を解決し、シームレスな開発体験を実現する方法を紹介します。
この記事では、実際の @opennextjs/cloudflare プロジェクト構成を参考に、ローカル開発からテスト実装、本番デプロイまでの一連の流れを解説します。
package.json
{
"dependencies" : {
"@libsql/client" : "^0.15.10" ,
"@opennextjs/cloudflare" : "^1.6.2" ,
"drizzle-orm" : "^0.44.4"
} ,
"devDependencies" : {
"drizzle-kit" : "^0.31.4" ,
"drizzle-seed" : "^0.3.1" ,
"vitest" : "^3.2.4" ,
"wrangler" : "^4.26.1"
}
}
この記事で構築する最終的な関連したプロジェクト構造は以下のようになります。
./
├── drizzle/
│ ├── migrations/
│ │ └── 0000_init.sql
│ └── schema.ts
├── tests/
│ ├── db.test.ts
│ └── db.ts
├── drizzle.config.ts
├── wrangler.jsonc
└── package.json
1. 必要なパッケージのインストール
まず、必要なパッケージをインストールします。
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit drizzle-seed vitest
2. Cloudflare D1 の設定
wrangler.jsonc に D1 データベースの情報を追記します。
と には、D1 データベースの「データベース名」と「データベースID」を指定します。値の確認・発行方法は後述します。
wrangler.jsonc
{
"$schema" : "node_modules/wrangler/config-schema.json" ,
"name" : "" ,
"main" : ".open-next/worker.js" ,
"compatibility_date" : "2025-03-01" ,
"compatibility_flags" : [ "nodejs_compat" , "global_fetch_strictly_public" ] ,
"d1_databases" : [
{
"binding" : "DB" ,
"database_name" : "" ,
"database_id" : "" ,
"migrations_dir" : "drizzle/migrations"
}
]
}
値の確認・発行方法
Cloudflare ダッシュボードから確認
Cloudflare Dashboard にログインし、 対象アカウントの「ストレージとデータベース」→「D1 SQL データベース」へ移動する
作成済みのデータベース一覧から、該当データベースを選択する
は一覧や詳細画面での表示名
は詳細画面の「UUID」
例: e216461a-74c3-40b2-8819-9fa351827304
Wrangler CLI で新規作成する場合
下記コマンド成功時に出力される JSON フィールドの値を用いる
database_name が
database_id が
npx wrangler d1 create my-app-dev
Successfully created DB 'my-app-dev' in region APAC
Created your new D1 database.
{
"d1_databases" : [
{
"binding" : "DB" ,
"database_name" : "my-app-dev" ,
"database_id" : "e216461a-74c3-40b2-8819-9fa351827304"
}
]
}
これらの値を wrangler.jsonc の該当箇所にコピー&ペーストしてください。
3. 環境別のデータベース設定
drizzle.config.ts に本番環境とローカル環境の設定を記載します。
環境の切り分けには NODE_ENV 環境変数を利用しており、NODE_ENV=production の場合は本番用、そうでない場合はローカル用の設定が適用されます。
drizzle.config.ts
import { readdirSync } from "fs"
import { defineConfig } from 'drizzle-kit'
const isProduction = process. env. NODE_ENV === 'production' ;
const sqliteDirPath = '.wrangler/state/v3/d1/miniflare-D1DatabaseObject' ;
const sqliteFilePath = readdirSync ( sqliteDirPath) . find ( file => file. endsWith ( '.sqlite' ) ) ;
const config = isProduction ? defineConfig ( {
out: './drizzle/migrations' ,
schema: './drizzle/schema.ts' ,
dialect: 'sqlite' ,
driver: 'd1-http' ,
dbCredentials: {
accountId: process. env. CLOUDFLARE_ACCOUNT_ID ! ,
databaseId: process. env. CLOUDFLARE_DATABASE_ID ! ,
token: process. env. CLOUDFLARE_D1_TOKEN ! ,
}
} ) : defineConfig ( {
out: './drizzle/migrations' ,
schema: './drizzle/schema.ts' ,
dialect: 'sqlite' ,
dbCredentials: {
url: ` ${ sqliteDirPath} / ${ sqliteFilePath! } ` ,
}
} ) ;
export default config;
!
wrangler のバージョンや環境によっては、SQLite ファイルの生成先ディレクトリが .wrangler/state/v3/d1/miniflare-D1DatabaseObject ではなく、.wrangler 配下の異なるパスになる可能性があります。
.wrangler 配下の SQLite ファイルのパスが不明な場合は、find コマンドで検索し特定できます。たとえば、以下のように実行すると、.sqlite 拡張子のファイルを探せます。
find .wrangler -type f -name "*.sqlite"
Cloudflare D1 の本番環境の設定には、以下の環境変数が必要です。
CLOUDFLARE_ACCOUNT_ID
CLOUDFLARE_DATABASE_ID
CLOUDFLARE_D1_TOKEN
本番環境設定の取得方法に関しては下記を参考にしてください。
https://zenn.dev/arafipro/articles/2024-07-24-drizzle-d1-table#drizzle.config.ts-1
4. データベーススキーマの定義
drizzle/schema.ts で以下の2つのテーブルを定義します。
drizzle/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core' ;
import { sql } from 'drizzle-orm' ;
export const conversations = sqliteTable ( 'conversations' , {
id: text ( 'id' ) . primaryKey ( ) ,
title: text ( 'title' ) ,
createdAt: integer ( 'created_at' , { mode: 'timestamp' } )
. notNull ( )
. default ( sql` ( unixepoch( ) ) ` ) ,
updatedAt: integer ( 'updated_at' , { mode: 'timestamp' } )
. notNull ( )
. default ( sql` ( unixepoch( ) ) ` ) ,
} ) ;
export const messages = sqliteTable ( 'messages' , {
id: text ( 'id' ) . primaryKey ( ) ,
conversationId: text ( 'conversation_id' )
. notNull ( )
. references ( ( ) => conversations. id, { onDelete: 'cascade' } ) ,
role: text ( 'role' , { enum : [ 'user' , 'system' ] } ) . notNull ( ) ,
content: text ( 'content' ) . notNull ( ) ,
createdAt: integer ( 'created_at' , { mode: 'timestamp' } )
. notNull ( )
. default ( sql` ( unixepoch( ) ) ` ) ,
} ) ;
5. テスト環境の構築
tests/db.ts でテスト用のユーティリティ関数を作成します。テスト用データベースの作成・マイグレーションの適用・テストデータの投入などを行う関数をまとめています。
tests/db.ts
import { createClient } from '@libsql/client/node' ;
import { drizzle } from 'drizzle-orm/libsql' ;
import * as schema from '../drizzle/schema' ;
import { readFileSync } from 'fs' ;
import { join } from 'path' ;
import { seed, reset } from 'drizzle-seed' ;
export async function createTestDatabase ( ) {
const client = createClient ( { url: ':memory:' } ) ;
const db = drizzle ( client, { schema } ) ;
const migrationsDir = join ( process. cwd ( ) , 'drizzle/migrations' ) ;
const migrationFiles = (
await Promise . resolve (
require ( 'fs' ) . readdirSync ( migrationsDir)
. filter ( ( file: string ) => file. endsWith ( '.sql' ) )
. sort ( )
)
) ;
for ( const file of migrationFiles) {
const migrationSQL = readFileSync ( join ( migrationsDir, file) , 'utf-8' ) ;
const statements = migrationSQL
. split ( '--> statement-breakpoint' )
. map ( stmt => stmt. trim ( ) )
. filter ( stmt => stmt. length > 0 ) ;
for ( const statement of statements) {
await client. execute ( statement) ;
}
}
return { client, db } ;
}
export async function seedTestDataWithDrizzleSeed (
db: any ,
options? : { count? : number ; seed? : number }
) {
const { count = 3 , seed: seedValue = 12345 } = options || { } ;
await seed ( db as any , schema, { count, seed: seedValue } ) . refine ( ( f) => ( {
conversations: {
count: 3 ,
columns: {
title: f. valuesFromArray ( {
values: [
'AI System Chat' ,
'Technical Q&A Session' ,
'Creative Writing Help' ,
'Code Review Discussion' ,
'Product Planning Talk'
]
} ) ,
}
} ,
messages: {
count: 6 ,
columns: {
role: f. valuesFromArray ( {
values: [ 'user' , 'system' ]
} ) ,
content: f. valuesFromArray ( {
values: [
'Hello! How can you help me today?' ,
'I\'m an AI system, happy to help with various tasks!' ,
'Can you explain how TypeScript interfaces work?' ,
'TypeScript interfaces define the shape of objects...' ,
'What are the best practices for React development?' ,
'Some key React best practices include using functional components...' ,
]
} ) ,
}
}
} ) ) ;
return db;
}
export async function resetDatabase ( db: any ) {
await reset ( db as any , schema) ;
}
6. テストの実装
tests/db.test.ts で実際のテストを実装します。このテストで、データベースの接続やクエリ、モデルの動作や挙動などが正しく機能するかどうかを検証します。
tests/db.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest' ;
import { conversations, messages } from '../drizzle/schema' ;
import { eq } from 'drizzle-orm' ;
import {
createTestDatabase,
seedTestDataWithDrizzleSeed,
resetDatabase
} from './db' ;
describe ( 'Drizzle Seed Test' , ( ) => {
let client: any ;
let db: any ;
beforeEach ( async ( ) => {
const testDb = await createTestDatabase ( ) ;
client = testDb. client;
db = testDb. db;
} ) ;
afterEach ( ( ) => {
client. close ( ) ;
} ) ;
it ( 'should seed test data using drizzle-seed' , async ( ) => {
await seedTestDataWithDrizzleSeed ( db, { count: 3 , seed: 12345 } ) ;
const allConversations = await db. select ( ) . from ( conversations) ;
expect ( allConversations. length) . toBeGreaterThan ( 0 ) ;
const allMessages = await db. select ( ) . from ( messages) ;
expect ( allMessages. length) . toBeGreaterThan ( 0 ) ;
for ( const message of allMessages) {
const conversation = allConversations. find ( ( c: any ) =>
c. id === message. conversationId
) ;
expect ( conversation) . toBeDefined ( ) ;
}
for ( const message of allMessages) {
expect ( [ 'user' , 'system' ] ) . toContain ( message. role) ;
}
} ) ;
it ( 'should reset database correctly' , async ( ) => {
await seedTestDataWithDrizzleSeed ( db) ;
let allConversations = await db. select ( ) . from ( conversations) ;
expect ( allConversations. length) . toBeGreaterThan ( 0 ) ;
await resetDatabase ( db) ;
allConversations = await db. select ( ) . from ( conversations) ;
const allMessages = await db. select ( ) . from ( messages) ;
expect ( allConversations) . toHaveLength ( 0 ) ;
expect ( allMessages) . toHaveLength ( 0 ) ;
} ) ;
} ) ;
予め package.json の scripts へ下記を登録しておきます。
package.json
{
"scripts" : {
"test" : "vitest run" ,
"db:gen" : "drizzle-kit generate" ,
"db:mgn" : "drizzle-kit migrate" ,
"db:mgn:prd" : "NODE_ENV=production drizzle-kit migrate" ,
"db:client" : "drizzle-kit studio"
}
}
1. DB のローカル環境構築
npx wrangler dev
npm run db:gen
npm run db:mgn
npm test
Drizzle には、Drizzle Studio という GUI で DB を確認可能なツールが存在します。 Drizzle Studio は下記コマンドで起動可能です。
2. DB の本番環境への反映
正常にコマンド実行が成功すれば、Cloudflare Dashboard の「D1 SQL データベース」から /tables コマンドで生成されたテーブルが確認できるはずです。
Drizzle のエコシステムを活用することで、OpenNext + Cloudflare D1 を使った開発・テスト・本番環境をシームレスに構築できました。
OpenNext を利用したい動機として、複雑な設定や環境構築に時間を取られることなく、迅速かつ効率的にアプリケーションの実装を進めたいというモチベで採用されることが大半だと考えます。
本記事の手法を使うことで、データベース周りの煩雑な設定や運用から解放されます。 そのため、アプリケーションロジックの実装に集中できます。爆速で開発を進めていきましょう。