Noh | エンジニア向け情報共有コミュニティ
Signup / Login

Hono + Cloudflare d1 + workers/pages + Prisma で作るWebアプリ

投稿日: 2025/02/20

Hono + Cloudflare d1 + workers/pages でWebアプリを作ります。

Cloudflare workers/pagesのどちらを選べばいいのかは難しいですが、静的アセット配信があるならPagesが良いらしいです(Workersにも似た機能が追加されてたと思いますが)。
API作るときはWorkers、Webサイト作るときはPagesだと思います。基本的に自分はWebサイト作るときはHonoじゃなくてAstro使ってます。

WorkersとPagesは統合が進んでますが、どちらかが使えなくなるような予定はないらしいです。

https://zenn.dev/yusukebe/articles/92fcb0ef03b151

今回はWorkersを使いますが、Pagesもhono createでpagesを選択すれば同じようにできるはずです。

ORMはDrizzle ORMが最近人気ですが、ActiveRecordになれた自分はPrismaの方が好みでした。歴史が長くある程度成熟してますし、楽な方が良いです。

参考になるリンク

参考になるドキュメントは以下です。

基本的にはCloudflare workers/pagesのドキュメントとHonoのGet Startedに沿って進めるだけです。Honoのドキュメントは洗練されていて悩むところは無いと思います。Prismaのマイグレーションに関わる部分だけそれらのドキュメントが少ないので、Prisma公式ドキュメントが参考になります。

Hono の公式ドキュメント Cloudflare Workers用 Getting Started

Hono 公式 Cloudflare Testing

Cloudflare 公式ドキュメント Query D1 from Hono

Query D1 using Prisma ORM

Prisma 公式ドキュメント Cloudflare D1

Honoプロジェクトの作成

npm create hono@latestコマンドでプロジェクトを作成します。
honoはTarget directory .とすることで、現在いるディレクトリにファイルを展開してくれるので便利です。

$ cd repo_name $ npm create hono@latest create-hono version 0.15.3 ? Target directory . ? Which template do you want to use? cloudflare-workers ? Directory not empty. Continue? yes ? Do you want to install project dependencies? yes ? Which package manager do you want to use? npm ✔ Cloning the template ✔ Installing project dependencies 🎉 Copied project files Get started with: cd .

するといくつかファイルが生成されます。

package.json

{ "name": "repo_name", "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy --minify" }, "dependencies": { "hono": "^4.7.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250109.0", "wrangler": "^3.101.0" } }

tsconfig.json

{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "lib": [ "ESNext" ], "types": [ "@cloudflare/workers-types/2023-07-01" ], "jsx": "react-jsx", "jsxImportSource": "hono/jsx" }, }

wrangler.jsonc

{ "$schema": "node_modules/wrangler/config-schema.json", "name": "repo_name", "main": "src/index.ts", "compatibility_date": "2025-02-18" // "compatibility_flags": [ // "nodejs_compat" // ], // "vars": { // "MY_VAR": "my-variable" // }, // "kv_namespaces": [ // { // "binding": "MY_KV_NAMESPACE", // "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // } // ], // "r2_buckets": [ // { // "binding": "MY_BUCKET", // "bucket_name": "my-bucket" // } // ], // "d1_databases": [ // { // "binding": "MY_DB", // "database_name": "my-database", // "database_id": "" // } // ], // "ai": { // "binding": "AI" // }, // "observability": { // "enabled": true, // "head_sampling_rate": 1 // } }

npm run devで起動できます。

テストを書く Vitestとcloudflare/vitest-pool-workersの導入

Cloudflare workersのテストはcloudflare/vitest-pool-workersを使用するのがよさそうなのでインストールします。その際、明示的にVitestもインストールすることが推奨されてます。
2025/02現在、cloudflare/vitest-pool-workersがVitest3に対応してないので、バージョン2を使ってます。そのうち対応すると思うので、バージョン3に対応してるか確認してください。

以下のコマンドでインストールします。

$ npm i -D @cloudflare/vitest-pool-workers $ npm install -D [email protected]

package.json

{ "name": "repo_name", "scripts": { "dev": "wrangler dev", "test": "vitest", "deploy": "wrangler deploy --minify" }, "dependencies": { "hono": "^4.7.1" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.6.16", "@cloudflare/workers-types": "^4.20250109.0", "vitest": "^2.1.9", "wrangler": "^3.101.0" } }

テストのimport部分で型エラーが出るので、tsconfig.json に"@cloudflare/vitest-pool-workers"を追加します。
includesやexcludesは不要です。

tsconfig.json

{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "lib": ["ESNext"], "types": [ "@cloudflare/workers-types/2023-07-01", "@cloudflare/vitest-pool-workers" ], "jsx": "react-jsx", "jsxImportSource": "hono/jsx" } }

最初のテストを書きます。

src/index.test.ts

import { describe, it, expect } from "vitest"; import { env } from "cloudflare:test"; import app from "./index"; describe("Example", () => { it("Should return 200 response", async () => { const res = await app.request("/", {}, env); expect(res.status).toBe(200); expect(await res.text()).toEqual("Hello Hono!"); }); });

npm run testでテストが実行されます。

CI の設定

Github Actionsを使ってテストが自動実行されるようにします。

react-dropzone-vvで使ってる設定の使いまわしです。

.github/workflows/ci.yml

name: CI on: push: jobs: ts-test: name: TS Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" - name: Cache node modules uses: actions/cache@v4 id: node_modules_cache_id env: cache-name: cache-node-modules with: path: ./node_modules key: ${{ runner.os }}-npm-${{ env.cache-name }}-${{ hashFiles('./package-lock.json') }} - name: npm ci if: ${{ steps.node_modules_cache_id.outputs.cache-hit != 'true' }} run: | npm ci - name: Run test run: | npm run test

Cloudflare d1とPrisma

まず、必要なPackageをインストールします。

$ npm install prisma --save-dev $ npm install @prisma/client $ npm install @prisma/adapter-d1

package.json

"dependencies": { "@prisma/adapter-d1": "^6.4.0", "@prisma/client": "^6.4.0", "hono": "^4.7.1" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.6.16", "@cloudflare/workers-types": "^4.20250109.0", "prisma": "^6.4.0", "vitest": "^2.1.9", "wrangler": "^3.101.0" }

npx prisma initコマンドを使ってprisma/schema.prismaを生成します。

$ npx prisma init --datasource-provider sqlite

prisma/schema.prisma

// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") }

プレビュー版の機能を使うために、previewFeatures = ["driverAdapters"]を追加します。

generator client { provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] }

d1の設定

Cloudflare d1の管理画面から新しいDBを作成します。
database_nameとdatabase_idが表示されるので、wrangler.jsoncへ追加します。

wrangler.jsonc

{ "$schema": "node_modules/wrangler/config-schema.json", "name": "repo_name", "main": "src/index.ts", "compatibility_date": "2025-02-18", "d1_databases": [ { "binding": "DB", "database_name": "test_db", "database_id": "xxxxxxxxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxx" } ]

Prismaのドキュメントに従って、最初のマイグレーション

Cloudflare d1はprisma migrate devコマンドに対応してないので、prisma migrate diff コマンドを使います。

今回は以下のようなユーザーテーブルを追加するマイグレーションを行います。

model User { id Int @id @default(autoincrement()) email String @unique name String? }

prisma/schema.prisma に追加します。

prisma/schema.prisma

// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String? }

wrangler を使ってマイグレーションファイルを作成します。

npx wrangler d1 migrations create __YOUR_DATABASE_NAME__ create_user_table

すると、空のmigrations/0001_create_user_table.sqlが作成されます。

migrations/0001_create_user_table.sqlへSQLを生成します。prisma migrate diffコマンドを使ってprisma/schema.prismaから生成します。

$ npx prisma migrate diff \ --from-empty \ --to-schema-datamodel ./prisma/schema.prisma \ --script \ --output migrations/0001_create_user_table.sql

migrations/0001_create_user_table.sql は以下のようになります。

migrations/0001_create_user_table.sql

-- CreateTable CREATE TABLE "User" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "email" TEXT NOT NULL, "name" TEXT ); -- CreateIndex CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

マイグレーションは以下のコマンドで適用します。

ローカルに適用する場合は

$ npx wrangler d1 migrations apply __YOUR_DATABASE_NAME__ --local

リモートに適用する場合は

$ npx wrangler d1 migrations apply __YOUR_DATABASE_NAME__ --remote

2回目以降のマイグレーション

初回と同じコマンドを使用すると同じファイルが上書きされてしまうので、コマンドを少し変えます。

公式ドキュメントと同様にPostテーブルを追加します。

prisma/schema.prisma

// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] } model Post { id Int @id @default(autoincrement()) title String author User @relation(fields: [authorId], references: [id]) authorId Int }

空のマイグレーションファイルを作成します。

$ npx wrangler d1 migrations create __YOUR_DATABASE_NAME__ create_post_table

migrations/0002_create_post_table.sqlが作成されます。

$ npx prisma migrate diff \ --from-local-d1 \ --to-schema-datamodel ./prisma/schema.prisma \ --script \ --output migrations/0002_create_post_table.sql

以下のようにSQLが生成されます。

migrations/0002_create_post_table.sql

-- CreateTable CREATE TABLE "Post" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "title" TEXT NOT NULL, "authorId" INTEGER NOT NULL, CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE );

マイグレーションは以下のコマンドで適用します。

ローカルに適用する場合は

$ npx wrangler d1 migrations apply __YOUR_DATABASE_NAME__ --local

リモートに適用する場合は

$ npx wrangler d1 migrations apply __YOUR_DATABASE_NAME__ --remote

ローカルのDBを削除したいとき

.wrangler/state/v3/d1/miniflare-D1DatabaseObject/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.sqliteのようなファイルを削除します。

d1と通信するHono api

試しにPostでユーザーを追加し、Getでユーザー一覧を取得するAPIを追加します。

index.ts

import { Hono } from "hono"; import { PrismaClient } from "@prisma/client"; import { PrismaD1 } from "@prisma/adapter-d1"; type Bindings = { DB: D1Database; }; const app = new Hono<{ Bindings: Bindings }>(); app.get("/users", async (c) => { const adapter = new PrismaD1(c.env.DB); const prisma = new PrismaClient({ adapter }); const users = await prisma.user.findMany(); return c.json(users); }); app.post("/users", async (c) => { const adapter = new PrismaD1(c.env.DB); const prisma = new PrismaClient({ adapter }); const user = await prisma.user.create({ data: { email: "[email protected]", name: "Elsa Prisma", }, }); return c.json(user); }); export default app;

以下のコマンドを実行して、APIをたたくと結果が返ってきます。

$ npx prisma generate
yosi

Noh を作ってるエンジニア。