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は統合が進んでますが、どちらかが使えなくなるような予定はないらしいです。
今回は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