Closed16

HonoX + Cloudflare D1 + Prisma で Web アプリケーションを構築

m.fukuzawam.fukuzawa

実行環境

  • macOS 13.6.3(22G436)
  • Cloudflare は Free プランで利用
$ pnpm --version
9.0.6
$ pnpm dlx wrangler --version
 ⛅️ wrangler 3.52.0
-------------------
m.fukuzawam.fukuzawa

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

ブラウザで確認。

m.fukuzawam.fukuzawa

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 を参照。
https://github.com/honojs/honox?tab=readme-ov-file#cloudflare-bindings

wrangler.toml
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
└───────┴────────┴──────────┴──────────┴────────────────────────────────────────────────────────────────────────────────────────┘
m.fukuzawam.fukuzawa

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 を追加しておく。

m.fukuzawam.fukuzawa

schema.prisma で driverAdapters Preview 機能を有効化

prisma/schema.prisma を以下のように修正。driverAdapters Preview 機能を有効にすると、 Prisma クライアントがアダプタを使用して D1 と通信できるようになるとのこと。
https://blog.cloudflare.com/prisma-orm-and-d1

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
+  previewFeatures = ["driverAdapters"]
}
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

ちなみに、以下のコマンドを実行するとインデントが自動でいい感じになる。

$ pnpm prisma format
m.fukuzawam.fukuzawa

モデルの追加

お試しに適当なモデルを追加してみる。

prisma/schema.prisma
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
+ }
m.fukuzawam.fukuzawa

マイグレーション

以下を実行し、マイグレーションファイルを作成。

$ pnpm dlx wrangler d1 migrations create $DATABASE_NAME create_tenant_table

この時点ではこういう感じで中身はコメントのみ。

migrations/0001_create_tenant_table.sql
-- 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

こうなる。

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
m.fukuzawam.fukuzawa

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 にこの辺りの仕組みを聞いてみると以下のような回答が返ってきた。
https://chat.openai.com/share/3e5c69fd-0a70-4698-82f0-13ad88ad1682

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 モデルの情報が追記されるのを確認。

m.fukuzawam.fukuzawa

global.d.ts に D1Database の Bindings を追加

以下のように修正を加える。

global.d.ts
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>
  }
}
m.fukuzawam.fukuzawa

vite.config.ts を編集

以下の記事を参考に変更を加えた。

https://zenn.dev/itaosan/articles/0246edeaf907c3#vite.config.tsの変更

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 時に外部化する (ブラウザ向けのバンドルに含めない) 依存関係を定義しているみたいな話らしい。

m.fukuzawam.fukuzawa

動作検証用のサンプル実装

SQL を実行するコードを追加してみる。 app/routes/index.ts を以下のように修正。

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 }
  );
});
m.fukuzawam.fukuzawa

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

m.fukuzawam.fukuzawa

デプロイ

$ 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 プランだとデプロイ失敗するかも?と思ってたけど特に問題なくデプロイが成功することを確認。

https://github.com/prisma/prisma/discussions/23646#discussioncomment-9059560
https://github.com/prisma/prisma/issues/23350

m.fukuzawam.fukuzawa

remote 動作確認

remote にもデータを入れる。

$ pnpm dlx wrangler d1 execute $DATABASE_NAME --command "insert into  \"Tenant\" (\"id\", \"name\") VALUES  (\"clvi19dgm000040xmnnsdz2fj\", \"Test Tenant\");" --remote

Cloudflare Pages の URL にアクセスして問題ないことを確認 🎉

このスクラップは17日前にクローズされました