この記事は「mofmof Advent Calendar 2023」4日目の記事です。
はじめに
気になっていたHonoやCloudflareを触ってみたいと思い、Honoのcloudflare-workersテンプレートを使って、ToDoのCRUD処理ができるWeb APIを作ってみました。
プロジェクト作成・準備
Honoのテンプレートの中からcloudflare-workersテンプレートを選んでプロジェクトを新規作成します。
$ npm create hono@latest hono-todo-app
create-hono version 0.3.2
✔ Using target directory … hono-todo-app
✔ Which template do you want to use? › cloudflare-workers
cloned honojs/starter#main to /Users/kmkkiii/Development/hono-todo-app
✔ Copied project files
$ npm i
次にwranglerに権限を付与しておきます。
wranglerはCloudflare開発のためのCLIツールです。
D1を操作したり、デプロイしたりするのに使います。
以下のコマンドを実行して開いたブラウザでCloudflareにログインして、Wranglerに権限を付与します。
$ npx wrangler login
⛅️ wrangler 3.18.0
-------------------
Attempting to login via OAuth...
Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?hogehoge
Successfully logged in.
D1 Database作成
D1はCloudflareのサーバーレスDBで、現在パブリックベータ版です。
以下のコマンドで作成できます。
[[d1_databases]]以下の情報は後ほどwrangler.tomlに設定するのでコピーしておきます。
$ npx wrangler d1 create my-database
✅ Successfully created DB 'my-database' in region APAC
Created your database using D1's new storage backend. The new storage backend is not yet recommended for production workloads, but backs up your data via
point-in-time restore.
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "my-database"
database_id = "${DATABASE_ID}"
wragler.tomlを編集します。
name = "hono-todo-app"
compatibility_date = "2023-01-01"
+ main = "src/index.ts"
# [vars]
# MY_VARIABLE = "production_value"
# [[kv_namespaces]]
# binding = "MY_KV_NAMESPACE"
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# [[r2_buckets]]
# binding = "MY_BUCKET"
# bucket_name = "my-bucket"
+ [[d1_databases]]
+ binding = "DB"
+ database_name = "my-database"
+ database_id = "${DATABASE_ID}"
WranglerにはMiniflareというローカルのCloudflare Workersシミュレータが統合されているらしく、ローカルでD1を動かせます!
$ npm run dev
> dev
> wrangler dev src/index.ts
⛅️ wrangler 3.18.0
-------------------
Your worker has access to the following bindings:
- D1 Databases:
- DB: my-database (${database_id})
⎔ Starting local server...
[mf:inf] Ready on http://localhost:62025
╭──────────────────────────────────────────────────────────────────────────────────────╮
│ [b open a browser[d] open Devtools[l] turn off local mode[c clear console,[x to exit │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────
Drizzle ORM導入
D1-ORM、Drizzle ORMがD1サポートしているみたいで、Prismaは現状D1未サポートのようです。
今回はDrizzle ORMを使います。
こちらの記事を参考にさせていただきました。
- インストール
$ npm i drizzle-orm
$ npm i -D drizzle-kit
- schema定義作成
- Drizzle-kitを使うとDBからスキーマ生成できるみたいですが今回は手動で作りました
import { sql } from "drizzle-orm";
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
export const todos = sqliteTable("todos", {
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
status: text("status", { enum: ["todo", "doing", "done"] }).default("todo"),
createdAt: integer("created_at", { mode: "timestamp" }).default(
sql`(strftime('%s', 'now'))`
),
updatedAt: integer("updated_at", { mode: "timestamp" }).default(
sql`(strftime('%s', 'now'))`
),
});
- drizzle.config.ts作成
- driverは
d1
を指定して、wrangler.tomlのパスとdb名を設定すると、$ npx drizzle-kit studio
でDrizzle Studioが利用できます-
d1
を指定すればSQLite driver(better-sqlite)なしでもローカルでmigrationが行えるようになっていました - Drizzle Studioでは、DBeaver等のGUIツールのようにデータベース操作を行えます
-
- driverは
import type { Config } from "drizzle-kit";
export default {
schema: "./src/schema.ts",
out: "./drizzle/migrations",
driver: "d1",
dbCredentials: {
wranglerConfigPath: "wrangler.toml",
dbName: "my-database",
},
} satisfies Config;
- migrationファイル作成
- ファイル名は自動で命名されました
- far slapstickはドタバタ騒ぎって意味らしいです
❯ npx drizzle-kit generate:sqlite
drizzle-kit: v0.20.6
drizzle-orm: v0.29.1
No config path provided, using default 'drizzle.config.ts'
Reading config file '/Users/kmkkiii/Development/hono-todo-app/drizzle.config.ts'
1 tables
todos 5 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/migrations/0000_far_slapstick.sql 🚀
- migrations_dirを設定
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "${database_id}"
+ migrations_dir = "drizzle/migrations"
- migration実行(local)
- 本番環境に対しては
--local
を外して実行
- 本番環境に対しては
$ npx wrangler d1 migrations apply my-database --local
Migrations to be applied:
┌────────────────────────────┐
│ name │
├────────────────────────────┤
│ 0000_far_slapstick.sql │
└────────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database my-database (${database_id}) from .wrangler/state/v3/d1:
┌────────────────────────────┬────────┐
│ name │ status │
├────────────────────────────┼────────┤
│ 0000_far_slapstick.sql │ ✅ │
└────────────────────────────┴────────┘
CRUD処理の実装
- Honoのインスタンス作成時にBindingsを渡すと、HonoのContextからD1のクライアントAPIメソッドにアクセスできるようになる
- Drizzle ORM
- SQLライクに書ける
- table.$inferSelectで型定義できて便利
- zodのスキーマ生成もできるっぽい
import { Hono } from "hono";
import { drizzle } from "drizzle-orm/d1";
import { todos } from "./schema";
import { eq } from "drizzle-orm";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.get("/", (c) => c.text("Hello Hono!"));
/**
* todos
*/
app.get("/todos", async (c) => {
const db = drizzle(c.env.DB);
const result = await db.select().from(todos).all();
return c.json(result);
});
/**
* create todo
*/
app.post("/todos", async (c) => {
const params = await c.req.json<typeof todos.$inferSelect>();
const db = drizzle(c.env.DB);
const result = await db
.insert(todos)
.values({ title: params.title })
.execute();
return c.json(result);
});
/**
* update todo
*/
app.put("/todos/:id", async (c) => {
const id = parseInt(c.req.param("id"));
if (isNaN(id)) {
return c.json({ error: "invalid ID" }, 400);
}
const params = await c.req.json<typeof todos.$inferSelect>();
const db = drizzle(c.env.DB);
const result = await db
.update(todos)
.set({ title: params.title, status: params.status })
.where(eq(todos.id, id));
return c.json(result);
});
/**
* delete todo
*/
app.delete("/todos/:id", async (c) => {
const id = parseInt(c.req.param("id"));
if (isNaN(id)) {
return c.json({ error: "invalid ID" }, 400);
}
const db = drizzle(c.env.DB);
const result = await db.delete(todos).where(eq(todos.id, id));
return c.json(result);
});
export default app;
デプロイ
migration実行済みなので以下のコマンドを実行するだけ!
$ npm run deploy
> deploy
> wrangler deploy --minify src/index.ts
⛅️ wrangler 3.18.0
-------------------
Your worker has access to the following bindings:
- D1 Databases:
- DB: my-database (${database_id})
Total Upload: 71.53 KiB / gzip: 22.25 KiB
Uploaded hono-todo-app (1.31 sec)
Published hono-todo-app (4.02 sec)
https://hono-todo-app.kmkkiii.workers.dev
Current Deployment ID: 33bb87f4-3fd2-418b-a1e7-c63c524846a6
感想
後半雑になってしまいましたが、サクッとWeb APIを作ることができました。
HonoにはGraphQLやJSXのMiddlewareもあるので、そのあたりも盛り込んでフルスタックな開発もしてみたいです。
余談
// Service Worker
app.fire()
// Module Worker
export default app
せっかくHonoを使っているのでapp.fire()
と書きたかったのですが、今はバインディング変数がローカライズされるという理由からModule Workerモード推奨のようだったので断念しました...
おわりに
ここまで読んでくださりありがとうございました!