HonoとCloudflare D1とDrizzle ORMを使ってWeb APIを作る

:::note info
この記事は「[mofmof Advent Calendar 2023](https://qiita.com/advent-calendar/2023/mofmof)」4日目の記事です。
:::

## はじめに

気になっていたHonoやCloudflareを触ってみたいと思い、Honoのcloudflare-workersテンプレートを使って、ToDoのCRUD処理ができるWeb APIを作ってみました。

## プロジェクト作成・準備

Honoのテンプレートの中からcloudflare-workersテンプレートを選んでプロジェクトを新規作成します。
```sh
$ 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に設定するのでコピーしておきます。

```sh
$ 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を編集します。
```diff
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を動かせます！
```sh
$ 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を使います。

こちらの記事を参考にさせていただきました。

https://zenn.dev/mizchi/articles/d1-drizzle-orm

- インストール
```sh
$ npm i drizzle-orm
$ npm i -D drizzle-kit 
```

- schema定義作成
    - Drizzle-kitを使うとDBからスキーマ生成できるみたいですが今回は手動で作りました
```ts:src/schema.ts
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ツールのようにデータベース操作を行えます
```ts:drizzle.config.ts
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はドタバタ騒ぎって意味らしいです
```sh
❯ 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を設定
```diff
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "${database_id}"
+ migrations_dir = "drizzle/migrations"
```

- migration実行(local)
    - 本番環境に対しては`--local`を外して実行
```sh
$ 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のスキーマ生成もできるっぽい

```ts:src/index.ts
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実行済みなので以下のコマンドを実行するだけ！

```sh
$ 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もあるので、そのあたりも盛り込んでフルスタックな開発もしてみたいです。

## 余談

```ts
// Service Worker
app.fire()

// Module Worker
export default app
```
せっかくHonoを使っているので`app.fire()`と書きたかったのですが、今はバインディング変数がローカライズされるという理由からModule Workerモード推奨のようだったので断念しました...

https://hono.dev/getting-started/cloudflare-workers#service-worker-mode-or-module-worker-mode

## おわりに

ここまで読んでくださりありがとうございました！