Cloudflare D1 を Hono + Drizzleで触ってみる 〜Zod による型安全なバリデーションを添えて〜
はじめに
こんにちは。しんりうです。
個人開発や 新規プロダクト開発を始める際、データベースの選定は特に重要な意思決定の 1 つです。選定にはあたってはコスト、運用の手間、パフォーマンスなど、考慮すべき点が多くあります。
また、現在私が取り組んでいるプロジェクトでは、Cloudflare のサービスを積極的に活用しています。
具体的には DNS 管理、Web アプリのホスティング、オブジェクトストレージの管理などを利用しており、さまざまな用途で Cloudflare のエコシステムの恩恵を受けています。
この状況を踏まえ、Cloudflare D1 は有効な選択肢の 1 つだと考えました。
本記事では、D1 とそれを取り巻くモダンな開発スタック(Hono + Drizzle)について、実装を交えながら紹介したいと思います。そして、実装例では Zod を用いた型安全なバリデーションも併せて紹介します。
Cloudflare D1 について
D1 とはなにか
一言で表すと、「Cloudflare が提供する世界中のエッジで動くシンプルで高速な SQLite データベース」です。
そのため、Cloudflare Workers/Pages/R2 など 同社のエコシステムとの親和性が高いことが強みです。
D1 は 2024 年4月に GA(Generally Available) しており、今後活用ケースが増えていくのではないかと思います。
D1 の特徴
特に「シンプル」、「速い」、「低コスト」である点が注目されています。
シンプル
- SQLite ベースなので学習コストが低く、既存のドライバや ORM が活用できる
-
wrangler
コマンド一つで環境を構築できる[1]
速い
- ユーザー近くのエッジで動作するためレイテンシを抑えられる
- Cloudflare のグローバルネットワークを活用した効率的なルーティング[2]
低コスト
- 無料で 5GB のストレージと月間 500 万クエリ/日まで利用でき、商用利用も可能
- 追加コストもストレージ(1GB/$0.75)とクエリ(250 億回/$0.50)のシンプルな従量課金制 [3]
Hono と Drizzle について
Web アプリケーションから D1 を操作するため、今回はエッジファーストな設計思想を持つ Hono と Drizzle を使用します。
それぞれについて簡単にご紹介します。
Hono とは
今とてもアツくて盛り上がっている Hono [炎] です。
軽量で高速な、 マルチランタイムで動作する Typescript 製 Web アプリケーションフレームワークです。
Node.js 依存の API ではなく Web 標準 API のみを使用しているため、ランタイムに依存しません。元々 Hono は Cloudflare Workers に特化した フレームワークとして開発されたという背景があるため、D1 とも相性抜群です [4]。
また、文法も Express ライクになっているため、最初の敷居も高くありません。
作者が日本人ということもあり国内外でコミュニティが盛り上がっているため、さまざまな所で紹介されています。
本記事ではこれ以上の説明は省略しますが、興味のある方はぜひ公式ドキュメントやコミュニティでの情報を参照してください。
Drizzle とは
SQL ライクにもリレーショナルにも書くことができる 軽量で高速な Typescripft 製 ORM[5] です。
SQL ライクとはどういうことかというと、次のような記述で DB を操作できるということです。
await db
.select()
.from(countries)
.leftJoin(cities, eq(cities.countryId, countries.id))
.where(eq(countries.id, 10));
また、一般的な ORM と同じくリレーショナルに書くこともできます。
const result = await db.query.users.findMany({
with: {
posts: true,
},
});
公式 docs にパフォーマンステストの結果もあるため、ぜひご覧ください。
実装
実際にハンズオンで実装していきたいと思います。
今回は便宜上、次のような順番で進めます。
- Hono のセットアップ
- D1 SQL Database の作成
- Drizzle のセットアップ
- マイグレーション
- Hono から D1 への接続
- CRUD 操作の実装 w/ Zod
- Cloudflare Workers へデプロイ
なお、動作環境については Node.js バージョンが 21.2.0
、各ライブラリのバージョンは以下のとおりです。
"hono": "^4.6.12",
"wrangler": "^3.88.0",
"drizzle-orm": "^0.37.0"
"drizzle-kit": "^0.29.1"
"@hono/zod-validator": "^0.4.1"
"drizzle-zod": "^0.5.1"
"zod": "^3.23.8"
ソースコードはこちらに公開しています。
Hono のセットアップ
Hono プロジェクトをcreate-hono
コマンドで作成します。
※ template として cloudflare-workers を選んでいる点に注意してください。
$ npx create-hono <任意のプロジェクト名>
> create-hono version 0.14.3
✔ Using target directory … .
? Which template do you want to use? cloudflare-workers
? Do you want to install project dependencies? yes
? Which package manager do you want to use? pnpm
✔ Cloning the template
✔ Installing project dependencies
🎉 Copied project files
create-hono
が成功すると、 src/index.ts
に以下のようなコードが生成されていると思います。
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => {
return c.text("Hello Hono!");
});
export default app;
ターミナルから pnpm run dev
コマンド等でローカルサーバーを立ち上げ、動作確認をしてみます。
$ curl -X GET "http://localhost:8787"
> Hello Hono!
D1 SQL Database の作成
Cloudflare のコンソール画面から作成することもできますが、今回は wrangler
コマンドで作成します。
$ npx wrangler d1 create <任意のデータベース名>
⛅️ wrangler 3.92.0
-------------------
✅ Successfully created DB <任意のデータベース名> in region WNAM
Created your new D1 database.
[[d1_databases]]
binding = "DB"
database_name = "<任意のデータベース名>"
database_id = "xxxx-xxxx-xxxx-xxxx"
次に、Cloudflare Workers が D1 に接続できるようバインディングを作成する必要があります。
上記のデータベース情報を元に、wrangler.toml
を以下のように更新してください。
[[d1_databases]]
binding = "DB"
database_name = "<任意のデータベース名>"
database_id = "xxxx-xxxx-xxxx-xxxx"
うまく設定できている状態で dev サーバーを立ち上げると、バインディングされている旨が表示されます。
$ pnpm run dev
> wrangler dev
⛅️ wrangler 3.92.0
-------------------
Your worker has access to the following bindings:
- D1 Databases:
- DB: <任意のデータベース名> (xxxx-xxxx-xxxx-xxxx) (local)
⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:8787
Drizzle のセットアップ
インストール
まずは 2 つのパッケージをインストールします。
-
drizzle-orm
: データベースを操作するための API を提供してくれるコアパッケージ -
drizzle-kit
: マイグレーションの実行などスキーマ管理を行うツールキット
Drizzle は コアパッケージと開発ツールを完全に分離することで、本番環境でのバンドルサイズを軽量に保ち、エッジでの動作を最適化できるという特徴があります。
これは例えば Prisma のようにスキーマからクライアントコードを生成・バンドルする方式と比較して、大きな利点といえます。
$ pnpm add drizzle-orm
$ pnpm add -D drizzle-kit
スキーマの定義
テーブル構造を schema.ts
に定義します。
今回は id
, title
, isCompleted
, updatedAt
, createdAt
をカラムとするシンプルな TODO テーブルを作成します。
タイムスタンプのデフォルト値はこちらを参考にしました。
import { sql } from "drizzle-orm";
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
export const todos = sqliteTable("todos", {
id: text("id").primaryKey().notNull(),
title: text("title").notNull(),
isCompleted: integer("is_completed", { mode: "boolean" }).default(false),
updatedAt: text("updated_at")
.notNull()
.default(sql`(current_timestamp)`)
.$onUpdateFn(() => sql`(current_timestamp)`),
createdAt: text("created_at")
.notNull()
.default(sql`(current_timestamp)`),
});
公式 docs に Example が載っているため、そちらも参考にされてください。
コンフィグファイルの作成
ここでは参照するスキーマファイル、マイグレーションファイルの出力先、ドライバ、DB の種類、機密情報などを設定します。
import type { Config } from "drizzle-kit";
export default {
schema: "./drizzle/schema.ts",
out: "./drizzle/migrations",
driver: "d1-http",
dialect: "sqlite",
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
token: process.env.CLOUDFLARE_D1_TOKEN!,
},
} satisfies Config;
機密情報は Cloudflare ダッシュボードから取得することができます。
詳細は下記リンクを参考にされてください。
マイグレーション
マイグレーションファイルの生成は以下コマンドになります。
$ npx drizzle-kit generate
1 tables
todos 4 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/migrations/0000_blushing_namorita.sql 🚀
マイグレーションの実行は以下コマンドになります。
$ npx drizzle-kit migrate
> [✓] migrations applied successfully!
ただし、ローカル環境の DB をマイグレーションする場合、Drizzle Kit
ではそのような機能が提供されていません。そのため、wrangler
コマンドを使う形になります。
$ npx wrangler d1 execute <任意のデータベース名> --file=./drizzle/migrations/<任意のマイグレーションファイル>.sql --local
Drizzle Studio で DB の中身を確認
Drizzle は GUI 上で DB を操作できるツールを提供しています。
D1 のベースとなっている SQLite はファイルベースのデータベースであり、MySQL や PostgreSQL のようなサーバー機能を持ちません。そのため、通常は DB クライアントツールからのリモート接続や DB 操作は困難です。
しかし Drizzle Studio を用いればこの問題を克服し、リモートで直感的な DB 操作を行うことができます [6]。
以下のコマンドで立ち上げることができます。
試しに todos
テーブルにレコードを追加してみてください。
$ npx dirzzle-kit studio
Hono から D1 への接続
マイグレーションが成功したら、次は実際に Hono アプリケーション上で Drizzle ORM を介して D1 に接続してみます。D1 へのアクセスは Hono のコンテキストを介して行うことができます。
上述したwrangler.toml
で バインディングが設定されていることを確認の上で、src/index.ts
を以下のように修正してください。
import { Hono } from "hono";
+ import { drizzle } from "drizzle-orm/d1";
+ import { todos } from "../drizzle/schema";
+ type Bindings = {
+ DB: D1Database;
+ };
- const app = new Hono();
+ const app = new Hono<{ Bindings: Bindings }>().basePath("api");
app.get("/", (c) => {
return c.text("Hello Hono!");
});
+ app.get("/todos", async (c) => {
+ const db = drizzle(c.env.DB);
+ const allTodos = await db.select().from(todos).all();
+
+ return c.json(allTodos, 200);
+ });
再度ローカルでサーバーを立ち上げて動作確認をしてみます。
ただし、ここでデフォルトではローカルの DB に接続されるようになっています。
リモートの DB に切り替えたい場合はコンソールでの出力メッセージにあるように、L
キーを押して切り替えてください。
todos
レコードが存在する場合、次のような結果を得ることができます。
$ curl -X GET "http://localhost:8787/api/todos" | jq
[
{
"id": "02a087ac-518d-48a6-8042-ef7769a9b4cc",
"title": "task 1",
"isCompleted": false,
"updatedAt": "2024-12-06 05:13:36",
"createdAt": "2024-12-06 01:18:53"
},
{
"id": "05402ca9-1084-49ee-8983-1eea38e5942b",
"title": "task 2",
"isCompleted": true,
"updatedAt": "2024-12-06 03:05:21",
"createdAt": "2024-12-06 02:53:42"
},
]
簡単な CRUD 操作 w/ Zod
追加で CRUD 操作を実装します。
また、ついでにzod
, drizzle-zod
, @hono/zod-validator
を用いたバリデーションも追加してみます。
Drizzle で定義したスキーマをもとにバリデーションをかけられるのがかなり強いです。
Zod 周りのインストール
まずは 関連パッケージをインストールします。
$ pnpm add zod drizzle-zod @hono/zod-validator
Zod スキーマの定義
次に src/types/todo.ts
に zod
, drizzle-zod
を用いてバリデーションスキーマを定義します。
createInsertSchema()
で Drizzle で定義したスキーマをもとに Zod スキーマを生成し、pick()
で各リクエストに必要なフィールドを抽出します。
import { z } from "zod";
import { todos } from "../../drizzle/schema";
import { createInsertSchema } from "drizzle-zod";
const insertTodoSchema = createInsertSchema(todos, {
id: z.string().uuid(),
title: z
.string()
.min(1, "タイトルは必須です")
.max(100, "タイトルは100文字以内にしてください"),
isCompleted: z.boolean(),
});
export const createTodoSchema = insertTodoSchema.pick({ title: true });
export const updateTodoSchema = insertTodoSchema.pick({
title: true,
isCompleted: true,
});
export const todoIdSchema = insertTodoSchema.pick({ id: true });
エンドポイント の追加
そして、src/indext.ts
に上で定義したスキーマと @hono/zod-validator
でバリデーションを行いつつ、追加のエンドポイントを生やします。
zValidator()
ミドルウェアによリバリデーションを行い、c.req.valid()
で型安全に取り出すことができます。
import { todos } from "../drizzle/schema";
import { createTodoSchema, todoIdSchema, updateTodoSchema } from "./types/todo";
import { eq } from "drizzle-orm";
import { zValidator } from "@hono/zod-validator";
~ 中略 ~
// Create TODO
app.post("/todos", zValidator("json", createTodoSchema), async (c) => {
const { title } = await c.req.valid("json");
const db = drizzle(c.env.DB);
const id = crypto.randomUUID();
const newTodo = await db
.insert(todos)
.values({ id, title })
.returning()
.get();
return c.json(newTodo, 201);
});
// Read Single TODO
app.get("/todos/:id", zValidator("param", todoIdSchema), async (c) => {
const db = drizzle(c.env.DB);
const id = c.req.valid("param").id;
const todo = await db.select().from(todos).where(eq(todos.id, id)).get();
if (!todo) {
return c.text("Not Found", 404);
}
return c.json(todo, 200);
});
// Update TODO
app.put(
"/todos/:id",
zValidator("param", todoIdSchema),
zValidator("json", updateTodoSchema),
async (c) => {
const db = drizzle(c.env.DB);
const id = c.req.param("id");
const body = await c.req.valid("json");
const updatedTodo = await db
.update(todos)
.set(body)
.where(eq(todos.id, id))
.returning()
.get();
if (!updatedTodo) {
return c.text("Not Found", 404);
}
return c.json(updatedTodo, 200);
}
);
// Delete TODO
app.delete("/todos/:id", zValidator("param", todoIdSchema), async (c) => {
const db = drizzle(c.env.DB);
const id = c.req.valid("param").id;
const deletedTodo = await db
.delete(todos)
.where(eq(todos.id, id))
.returning()
.get();
if (!deletedTodo) {
return c.text("Not Found", 404);
}
return c.newResponse(null, { status: 204 });
});
export default app;
Cloudflare Workers へデプロイ
最後に Cloudflare Workers にデプロイします。
ぜひ実際に発行されたエンドポイントを叩いて動作確認をしてみてください。
$ pnpm run deploy
⛅️ wrangler 3.92.0
-------------------
Total Upload: 89.48 KiB / gzip: 27.23 KiB
Worker Startup Time: 2 ms
Your worker has access to the following bindings:
- D1 Databases:
- DB: <データベース名> (<データベースID>)
Uploaded <プロジェクト名> (2.82 sec)
Deployed <プロジェクト名> triggers (0.48 sec)
https://<プロジェクト名>.<ユーザー名>.workers.dev
Current Version ID: yyyy-yyyy-yyyy-yyyy
おわりに
今回は サーバーレス SQL データーベースな Cloudflare D1 を、 エッジ動作に最適な Web アプリケーションフレームワーク Hono と ORM ライブラリ Drizzle を使って触ってみました。
また、CRUD 操作の実装では Zod による型安全なバリデーションもご紹介しました。
本来 D1 の紹介を主軸にするつもりでしたが、実装内容の都合などにより Drizzle, Zod 等に関する情報も増し増しになってしまいました。
もしどなたかの参考になれば幸いです。
また、DeNA 25 新卒 Advent Calendar 2024 の他の記事もぜひご覧ください。
ここまで読んでいただきありがとうございました。
-
実行には Cloudflare アカウントの認証が必要です。 ↩︎
-
現在 D1 は SQLite のスナップショット分離によって一貫性を実現しています。リードレプリカを用意しているわけではありませんが、Cloudflare のネットワークインフラを活用することで効率的なアクセスを可能にしています。(https://blog.cloudflare.com/ja-jp/building-d1-a-global-database/) ↩︎
-
実は D1 の内部でも Hono が使われているそうです。 ↩︎
-
別の TS 製 ORM として Prisma も候補に上がると思いますがD1 の Prisma サポートは現在プレビュー版となっています。そのため採用を見送りました。 ↩︎
-
ただし Drizzle Studio でローカルの DB に接続するには、ファイルパスの指定などひと工夫必要になります。筆者はその代わりに TablePlus という GUI ツールを使っています。 ↩︎
Discussion