😽
Hono + Drizzle + Cloudflare D1の構成を試してみた
HonoとDrizzleを使ってCloudflare D1にアクセスする構成を試してみました。この記事では、マイグレーションからデプロイまでの手順を共有します。
手順
プロジェクトの作成
npm create hono@latest hono-drizzle
今回はcloudflare-pages
を選択します。
インストール
Drizzleのパッケージをインストールします。
npm i drizzle-orm
npm i -D drizzle-kit
バリデータとしてZodとZodのプラグインをインストールします。
npm i zod @hono/zod-validator
DBの作成
d1にDBを作成します。
npx wrangler d1 create hono-drizzle
出力されたdbの接続情報をwrangler.toml
に追加します。
drizzleの設定
drizzle.config.ts
import type { Config } from "drizzle-kit";
export default {
schema: "./src/schema/*",
out: "./migrations",
driver: "d1",
dbCredentials: {
wranglerConfigPath: "./wrangler.toml",
dbName: "hono-drizzle"
}
} satisfies Config;
DBスキーマの作成
今回はユーザーの
src/schema/users.ts
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: text("id").primaryKey().notNull(),
displayId: text("displayId").notNull(),
name: text("name").notNull(),
});
マイグレーションファイルの作成
以下のコマンドでマイグレーションファイルを生成します。
npx drizzle-kit generate:sqlite
マイグレーションの実施
以下のコマンドでマイグレーションを実施します。
npx wrangler d1 migrations apply hono-drizzle
アプリケーションコードの作成
続いて、CRUDを実装していきます。
型定義
src/types/users.ts
import { z } from "zod";
export const UserName = z.string()
.min(3, "ユーザー名は3文字以上の必要があります。")
.max(32, "ユーザー名は32文字以下の必要があります。");
export type UserName = z.infer<typeof UserName>;
export const DisplayId = z.string()
.min(3, "ユーザー名は3文字以上の必要があります。")
.max(32, "ユーザー名は32文字以下の必要があります。")
.regex(/^[a-zA-Z0-9-]+$/, "ユーザー名は英数字とハイフンのみが使用できます。");
export type DisplayId = z.infer<typeof DisplayId>;
Create
src/api/users/createUser.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { drizzle } from "drizzle-orm/d1";
import { users } from "../../schema/users";
import { nanoid } from "nanoid/non-secure";
import { DisplayId, UserName } from "../../types/users";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.post ("/", zValidator(
"json",
z.object({
displayId: DisplayId,
name: UserName,
}),
), async (c) => {
const db = drizzle(c.env.DB);
const { displayId, name } = c.req.valid("json");
const id = nanoid();
try {
await db.insert(users).values({ id, displayId, name });
return c.body("", 201);
} catch (e) {
if (e instanceof Error) {
const uniqueConstraintDisplayIdError = "D1_ERROR: UNIQUE constraint failed: users.displayId";
if (e.message === uniqueConstraintDisplayIdError) {
return c.json({
error: "このidはすでに使用されています"
}, 409);
}
console.error({ message: "エラー", errorMessage: e.message });
return c.body("", 500);
}
console.error({ message: "不明なエラー" });
return c.body("", 500);
}
});
export default app;
Read
src/api/users/findUser.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { drizzle } from "drizzle-orm/d1";
import { users } from "../../schema/users";
import { eq } from "drizzle-orm/sqlite-core/expressions";
import { DisplayId } from "../../types/users";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.get(
"/:displayId",
zValidator(
"param",
z.object({
displayId: DisplayId,
})
),
async (c) => {
try {
const db = drizzle(c.env.DB);
const displayId = c.req.valid("param").displayId;
const result = await db.select().from(users).where(eq(users.displayId, displayId));
const selectedUser = result.at(0);
if ( selectedUser === undefined) {
return c.body("", 404);
}
return c.json({ user: {
displayId: selectedUser.displayId,
name: selectedUser.name,
}});
} catch (e) {
if (e instanceof Error) {
console.error({ message: "エラー", errorMessage: e.message });
return c.body("", 500);
}
console.error({ message: "不明なエラー" });
return c.body("", 500);
}
}
);
export default app;
Update
src/api/users/patch.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { drizzle } from "drizzle-orm/d1";
import { users } from "../../schema/users";
import { eq } from "drizzle-orm/sqlite-core/expressions";
import { DisplayId, UserName } from "../../types/users";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.put(
"/:displayId",
zValidator(
"param",
z.object({
displayId: DisplayId,
})
),
zValidator(
"json",
z.object({
displayId: DisplayId,
name: UserName
})
),
async (c) => {
try {
const db = drizzle(c.env.DB);
const displayId = c.req.valid("param").displayId;
const newData = c.req.valid("json");
console.log(newData);
const result = await db.update(users).set({
displayId: newData.displayId,
name: newData.name
}).where(eq(users.displayId, displayId));
if (result.meta.changes === 0) {
console.error({ message: "更新対象が見つかりませんでした", displayId: displayId });
return c.body(null, 404);
}
console.log({isSuccess: result.success, changes: result.meta.changes, data: JSON.stringify(result)});
return c.body(null, 204);
} catch (e) {
if (e instanceof Error) {
console.error({ message: "エラー", errorMessage: e.message, obj: JSON.stringify(e) });
if (e.message === "D1_ERROR: UNIQUE constraint failed: users.displayId") {
return c.json({error: "このidはすでに使用されています"}, 409);
}
return c.body(null, 500);
}
console.error({ message: "不明なエラー" });
return c.body(null, 500);
}
}
);
export default app;
Delete
src/api/users/delete.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { drizzle } from "drizzle-orm/d1";
import { users } from "../../schema/users";
import { eq } from "drizzle-orm/sqlite-core/expressions";
import { DisplayId } from "../../types/users";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.delete(
"/:id",
zValidator(
"param",
z.object({
id: DisplayId,
})
),
async (c) => {
try {
const db = drizzle(c.env.DB);
const displayId = c.req.valid("param").id;
const result = await db.delete(users).where(eq(users.displayId, displayId));
console.log({isSuccess: result.success, changes: result.meta.changes, data: JSON.stringify(result)});
if (result.meta.changes === 0) {
console.error({ message: "削除対象が見つかりませんでした", displayId: displayId });
return c.body(null, 404);
}
return c.body(null, 204);
} catch (e) {
if (e instanceof Error) {
console.error({ message: "エラー", errorMessage: e.message, obj: JSON.stringify(e) });
return c.body(null, 500);
}
console.error({ message: "不明なエラー" });
return c.body(null, 500);
}
}
);
export default app;
ルーティング
作成したハンドラーをルーティングに設定します。
src/index.ts
import { Hono } from "hono";
import api from "./api";
import { logger } from "hono/logger";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.use(logger());
app.route("/api", api);
app.get("/health", (c) => {
return c.text("OK");
});
export default app;
src/api/index.ts
import { Hono } from "hono";
import users from "./users";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.route("/users", users);
export default app;
src/api/users/index.ts
import findUser from "./findUser";
import createUser from "./createUser";
import updateUser from "./updateUser";
import deleteUser from "./deleteUser";
import { Hono } from "hono";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.route("/", findUser);
app.route("/", createUser);
app.route("/", updateUser);
app.route("/", deleteUser);
export default app;
デプロイ
npm run deply
動作確認
リクエストをして動作を確認します。Readに関しては100msぐらいで、Writeに関しては150msから200msぐらいでレスポンスが返ってきました😊
感想
ORMが使えると、クエリごとに型定義をしなくてもよくなるのは好印象です。
一方で、Drizzleを使う必要がある煩わしさはあるかもしれません。UNIQUE制約のエラーハンドリングに関しはあまり綺麗ではないので、ほかに良い方法があればなと思います。Prismaを試した方がいい?
また、Cloudflare D1のマイグレーションにはリバート機能が無いのもこれからの期待だな~と思います。
Discussion