LoginSignup
2

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

Last updated at Posted at 2023-12-03

この記事は「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からスキーマ生成できるみたいですが今回は手動で作りました
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ツールのようにデータベース操作を行えます
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はドタバタ騒ぎって意味らしいです
❯ 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のスキーマ生成もできるっぽい
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実行済みなので以下のコマンドを実行するだけ!

$ 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モード推奨のようだったので断念しました...

おわりに

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2