HonoX + Cloudflare D1 + Prisma で Web アプリケーションを構築
参考URL
実行環境
- macOS 13.6.3(22G436)
- Cloudflare は Free プランで利用
$ pnpm --version
9.0.6
$ pnpm dlx wrangler --version
⛅️ wrangler 3.52.0
-------------------
create-hono でアプリケーションを作成
x-basic
を選択。
$ pnpm create hono@latest b2b-saas-authorization-sample
create-hono version 0.7.0
✔ Using target directory … b2b-saas-authorization-sample
? Which template do you want to use? x-basic
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? pnpm
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd b2b-saas-authorization-sample
自動生成されたファイルの確認。
$ cd b2b-saas-authorization-sample
$ tree . -I node_modules
.
├── app
│ ├── client.ts
│ ├── global.d.ts
│ ├── islands
│ │ └── counter.tsx
│ ├── routes
│ │ ├── _renderer.tsx
│ │ └── index.tsx
│ └── server.ts
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
└── vite.config.ts
3 directories, 10 files
サーバーを起動。
$ pnpm dev
> basic@ dev /Users/masashifukuzawa/works/my-repositories/b2b-saas-authorization-sample
> vite
(!) Could not auto-determine entry point from rollupOptions or html files and there are no explicit optimizeDeps.include patterns. Skipping dependency pre-bundling.
VITE v5.2.10 ready in 1282 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
ブラウザで確認。
初期状態で first commit しておく。
$ git init
$ git add .
$ git commit -m "pnpm create hono@latest b2b-saas-authorization-sample"
以下のRepositoryに対してPush。
$ git remote add origin git@github.com:MasashiFukuzawa/b2b-saas-authorization-sample.git
$ git push -u origin main
Cloudflare D1 に新規データベースを作成
$ export DATABASE_NAME=b2b_saas_authorization_sample
$ pnpm dlx wrangler d1 create $DATABASE_NAME
⛅️ wrangler 3.52.0
-------------------
✅ Successfully created DB 'b2b_saas_authorization_sample' in region APAC
Created your new D1 database.
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "b2b_saas_authorization_sample"
database_id = "xxxxx"
ルートディレクトリに wrangler.toml
を追加し、以下のように編集。pages_build_output_dir
は Cloudflare Pages にデプロイする時に必要とのこと。HonoX の README を参照。
name = "authorization-sample"
compatibility_date = "2023-12-01"
pages_build_output_dir = "./dist"
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "b2b_saas_authorization_sample"
database_id = "xxxxx"
コマンドを実行して接続を確認。
$ pnpm dlx wrangler d1 execute $DATABASE_NAME --command "select * from sqlite_schema"
⛅️ wrangler 3.52.0
-------------------
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database b2b_saas_authorization_sample (xxxxx) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
┌───────┬────────┬──────────┬──────────┬────────────────────────────────────────────────────────────────────────────────────────┐
│ type │ name │ tbl_name │ rootpage │ sql │
├───────┼────────┼──────────┼──────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ table │ _cf_KV │ _cf_KV │ 2 │ CREATE TABLE _cf_KV ( │
key TEXT PRIMARY KEY,
value BLOB
) WITHOUT ROWID
└───────┴────────┴──────────┴──────────┴────────────────────────────────────────────────────────────────────────────────────────┘
Prisma のセットアップ
必要なライブラリをインストール。
$ pnpm add -D prisma
$ pnpm add @prisma/client @prisma/adapter-d1
prisma init
を実行。
$ pnpm dlx prisma init --datasource-provider sqlite
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
┌────────────────────────────────────────────────────────────────┐
│ Developing real-time features? │
│ Prisma Pulse lets you respond instantly to database changes. │
│ https://pris.ly/cli/pulse │
└────────────────────────────────────────────────────────────────┘
上記に記載の通り .gitignore
に .env
を追加しておく。
schema.prisma で driverAdapters Preview 機能を有効化
prisma/schema.prisma
を以下のように修正。driverAdapters Preview 機能を有効にすると、 Prisma クライアントがアダプタを使用して D1 と通信できるようになるとのこと。
generator client {
provider = "prisma-client-js"
+ previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
ちなみに、以下のコマンドを実行するとインデントが自動でいい感じになる。
$ pnpm prisma format
モデルの追加
お試しに適当なモデルを追加してみる。
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
+ model Tenant {
+ id String @id @default(cuid())
+ name String
+ }
マイグレーション
以下を実行し、マイグレーションファイルを作成。
$ pnpm dlx wrangler d1 migrations create $DATABASE_NAME create_tenant_table
この時点ではこういう感じで中身はコメントのみ。
-- Migration number: 0001 2024-04-28T08:17:37.164Z
上記で作成した空のマイグレーションファイルに schema.prisma
の内容を変換して出力。
$ pnpm dlx prisma migrate diff --from-empty --to-schema-datamodel ./prisma/schema.prisma --script --output migrations/0001_create_tenant_table.sql
こうなる。
-- CreateTable
CREATE TABLE "Tenant" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL
);
D1 に対してマイグレーションを実行する。
まずは local に対して実行。
$ pnpm dlx wrangler d1 migrations apply $DATABASE_NAME --local
次に remote に対して実行。
$ pnpm dlx wrangler d1 migrations apply $DATABASE_NAME --remote
Prisma クライアントを再生成
これを実行するとIDE上で型補完が効くようになる。
$ pnpm dlx prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
✔ Generated Prisma Client (v5.13.0) to ./node_modules/.pnpm/@prisma+client@5.13.0_prisma@5.13.0/node_modules/@prisma/client in 171ms
Start using Prisma Client
...
ちなみに ChatGPT にこの辺りの仕組みを聞いてみると以下のような回答が返ってきた。
Prismaが自動生成する型情報は、通常、あなたのプロジェクトディレクトリ内のnode_modules/@prisma/clientディレクトリに保持されます。ここにはPrisma Clientが格納されており、prisma generateコマンドを実行することによって生成される型定義ファイルが含まれます。このプロセスは、Prismaのスキーマファイルに基づいて行われます。
生成された型は、特に.d.ts(TypeScriptの型定義ファイル)として存在し、これによってTypeScriptコンパイラがPrisma Clientを使用する際の型チェックを行うことができます。この型定義ファイルは、Prisma ClientのAPIを使用する際に、正しい型情報が提供され、IDEの自動補完やコードナビゲーションが効率的に機能するように設計されています。
確かに prisma generate
を実行すると ./node_modules/.pnpm/@prisma+client@5.13.0_prisma@5.13.0/node_modules/@prisma/client/index.js
の中身が更新されて上記で定義した Tenant モデルの情報が追記されるのを確認。
global.d.ts に D1Database の Bindings を追加
以下のように修正を加える。
import {} from 'hono'
type Head = {
title?: string
}
declare module 'hono' {
interface Env {
Variables: {}
< Bindings: {}
> Bindings: {
+ DB: D1Database;
+ }
}
interface ContextRenderer {
(content: string | Promise<string>, head?: Head): Response | Promise<Response>
}
}
vite.config.ts を編集
以下の記事を参考に変更を加えた。
import pages from '@hono/vite-cloudflare-pages';
import adapter from '@hono/vite-dev-server/cloudflare';
import honox from 'honox/vite';
import client from 'honox/vite/client';
import { defineConfig } from 'vite';
const baseConfig = {
ssr: {
external: ['@prisma/client', '@prisma/adapter-d1'],
},
};
export default defineConfig(({ mode }) => {
if (mode === 'client') {
return {
...baseConfig,
plugins: [client()],
};
} else {
return {
...baseConfig,
plugins: [
honox({
devServer: { adapter },
}),
pages(),
],
};
}
});
普段は Python を読み書きしている人間なので、この辺の設定は正直ちゃんと理解できてないが、 ChatGPT に聞くと Cloudflare Adapter を利用 + SSR 時に外部化する (ブラウザ向けのバンドルに含めない) 依存関係を定義しているみたいな話らしい。
動作検証用のサンプル実装
SQL を実行するコードを追加してみる。 app/routes/index.ts
を以下のように修正。
import { PrismaD1 } from '@prisma/adapter-d1';
import { PrismaClient } from '@prisma/client';
import { css } from 'hono/css';
import { createRoute } from 'honox/factory';
import Counter from '../islands/counter';
const className = css`
font-family: sans-serif;
`;
export default createRoute(async (c) => {
const adapter = new PrismaD1(c.env.DB);
const prisma = new PrismaClient({ adapter });
const tenants = await prisma.tenant.findMany();
const name = c.req.query('name') ?? 'Hono';
const tenantName = tenants[0].name;
return c.render(
<div class={className}>
<h1>Hello, {tenantName}!</h1>
<Counter />
</div>,
{ title: name }
);
});
local 動作確認
適当なテナントデータを作成。
ここでは Tenant.name
を Test Tenant とした。
$ pnpm dlx wrangler d1 execute $DATABASE_NAME --command "insert into \"Tenant\" (\"id\", \"name\") VALUES (\"clvi19dgm000040xmnnsdz2fj\", \"Test Tenant\");" --local
その後、ブラウザで動作確認。
$ pnpm dev
デプロイ
$ pnpm run deploy
...
The project you specified does not exist: "authorization-sample". Would you like to create it?"
❯ Create a new project
✔ Enter the production branch name: … main
✨ Successfully created the 'authorization-sample' project.
🌎 Uploading... (4/4)
✨ Success! Uploaded 4 files (3.14 sec)
✨ Compiled Worker successfully
✨ Uploading Worker bundle
✨ Deployment complete! Take a peek over at https://xxxxx.authorization-sample.pages.dev
デプロイ前にたまたま以下の Discussion と Issue を見つけて、 Cloudflare の Free プランだとデプロイ失敗するかも?と思ってたけど特に問題なくデプロイが成功することを確認。
remote 動作確認
remote にもデータを入れる。
$ pnpm dlx wrangler d1 execute $DATABASE_NAME --command "insert into \"Tenant\" (\"id\", \"name\") VALUES (\"clvi19dgm000040xmnnsdz2fj\", \"Test Tenant\");" --remote
Cloudflare Pages の URL にアクセスして問題ないことを確認 🎉