Monorepo (Hono + React)¶
Workspaces Hono Vite+/React Drizzle Validation
Hono バックエンド、Vite+/React クライアント、そして共有型パッケージを 1 つのワークスペースで連携させる 3 パッケージ構成。サーバー・クライアント・両者をつなぐコントラクトを明確に分離したい場合、かつワークスペースツーリングのコストを許容できる場合に選ぶ。単一の package.json で済むなら、代わりに Full-Stack (single package) テンプレートを検討すること。
共有パッケージは ReScript の型を Shared.* 名前空間でエクスポートする。サーバーは @<project>/shared に依存し、ルートハンドラ内でそれらの型を使う。クライアントは同じワークスペースに依存し、React コンポーネント内で型を消費する。packages/shared/src/Api.res のワイヤフォーマット record を編集すると、双方のビルドが整合するまで 両側 で破壊される —— 構造的型付けこそがこのテンプレートの肝である。
生成内容¶
my-monorepo/
├── package.json # root: workspaces + dev/dev:server/dev:client/test fan-out
├── pnpm-workspace.yaml # only when PM = pnpm; npm/yarn/bun use the workspaces field
├── packages/
│ ├── shared/
│ │ ├── rescript.json # name = @<project>/shared, namespace = "Shared"
│ │ ├── package.json
│ │ └── src/
│ │ ├── Types.res # domain records (Shared.Types.user)
│ │ └── Api.res # wire-format records (Shared.Api.createUserReq)
│ ├── server/
│ │ ├── rescript.json # bs-deps: @rescript/core, @<project>/shared, validation
│ │ ├── package.json # Hono + Drizzle + libsql + validation
│ │ ├── drizzle.config.ts
│ │ ├── vitest.config.mjs # loads vitest.setup.mjs
│ │ ├── vitest.setup.mjs # pins DATABASE_URL=:memory:
│ │ ├── .env.example # DATABASE_URL=file:./data/app.db
│ │ └── src/
│ │ ├── ServerMain.res # entry — calls Server.start()
│ │ ├── Server.res # Hono app: GET/POST /api/users, GET /api/hello
│ │ ├── Hono.res # bindings shared with other Hono templates
│ │ ├── HonoNodeServer.res
│ │ ├── Schema.res # Drizzle SQLite schema (users table)
│ │ ├── Db.res # libsql client + query helpers
│ │ ├── Validation.res # zod or sury — selected in the wizard
│ │ └── __tests__/Server.test.mjs # vitest: app.request("/api/hello") returns 200
│ └── client/
│ ├── rescript.json # bs-deps: ..., @rescript/react, @<project>/shared
│ ├── package.json # Vite+ + React + workspace dep on shared
│ ├── index.html
│ ├── vite.config.mjs # proxies /api/* to the Hono server
│ └── src/
│ ├── Main.res # React root render
│ ├── App.res # users form + list — uses Shared.Types.user
│ ├── ApiClient.res # fetch wrapper, types pinned to Shared.Api.*
│ └── __tests__/ApiClient.test.mjs
├── README.md # workspaces note + Database + Vite+ + Networking sections
├── LICENSE # MIT, holder = project name
├── .nvmrc # Node 24
├── .gitignore # adds dist/, .vite/, packages/*/dist/, packages/*/data/, .env
├── .editorconfig
└── .github/
├── dependabot.yml
└── workflows/ci.yml # install + test fan-out
ウィザードオプション¶
オプション |
効果 |
|---|---|
Project name |
ルートの npm |
Package manager |
npm / pnpm / yarn / bun のいずれか。ワークスペース宣言の形、ワークスペースごとのコマンド構文、 |
Validation library |
|
パッケージマネージャの選択は、インストールコマンドに加えて、設計上重要な 3 点に影響する:
観点 |
pnpm |
yarn |
npm |
bun |
|---|---|---|---|---|
ワークスペース宣言 |
|
|
|
|
ワークスペース単位コマンド |
|
|
|
|
ワークスペース依存バージョン |
|
|
|
|
主要な依存¶
ルート¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
サーバーとクライアントの dev ウォッチャーを並列で実行する |
|
packages/server¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ+ランタイム |
|
|
共有型パッケージへのワークスペース依存 |
|
|
HTTP フレームワーク |
|
|
Node HTTP アダプタ |
|
|
バリデーションバックエンド |
|
|
libsql クライアント(SQLite 互換、Turso 対応) |
|
|
型安全な SQL ビルダー |
|
|
マイグレーションジェネレータ |
|
|
テストランナー |
|
|
カバレッジプロバイダ |
|
|
dev 用に |
|
packages/client¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ+ランタイム |
|
|
React バインディング |
|
|
共有型パッケージへのワークスペース依存 |
|
|
React ランタイム |
|
|
Vite+ ビルドツール(vite + vitest + 設計済みデフォルト) |
|
|
Vite+ ランタイムコア |
|
|
基盤となる Vite(フォールバック依存として保持) |
|
|
React fast-refresh プラグイン |
|
|
テストランナー+カバレッジ |
|
|
|
|
主要なファイル¶
packages/server/src/Server.res¶
3 つのルート(GET /api/hello、GET /api/users、POST /api/users)を持つ Hono アプリ。POST ルートはボディを Validation.parseCreateUserReq でバリデーションしたうえで、Drizzle 経由で書き込む:
app->Hono.post("/api/users", async ctx => {
let raw = await ctx->Hono.req->Hono.jsonBody
switch Validation.parseCreateUserReq(raw) {
| Error(msg) => ctx->Hono.status(400)->Hono.json({"error": msg})
| Ok(payload) =>
let inserted =
await Db.db
->Db.insert(Schema.users)
->Db.values({"name": payload.name, "email": payload.email})
->Db.returning
ctx->Hono.status(201)->Hono.json(inserted->Array.get(0))
}
})
CORS フックはコメントアウトされたままになっている。Vite+ が /api/* をサーバーにプロキシして同一オリジンになるため、dev ワークフローでは不要である。
packages/server/src/ServerMain.res¶
中身は Server.start() の 1 行のみ。この分割があることで、vitest.setup.mjs がテストが import("../Server.res.mjs") する前に DATABASE_URL=:memory: を固定でき、ポートにバインドせずに app.request("/api/hello") を呼び出せる。
packages/server/vitest.setup.mjs¶
process.env.DATABASE_URL = ":memory:";
テストが Server.res.mjs(推移的に Db.res)を import する前に URL を固定することで、libsql クライアントはテスト一時ディレクトリに存在しない ./data/app.db を読もうとせず、インメモリのデータベースを開くようになる。
packages/client/src/App.res と ApiClient.res¶
App.res は小さなユーザーフォームとリストである。重要な点は Shared.Api.createUserReq の型注釈であり、packages/shared にずれが生じればクライアントとサーバーの 両方 でコンパイルエラーが発生する。これは破壊が起きてほしい瞬間に起きるという、まさに望ましい挙動である:
let req: Shared.Api.createUserReq = {name, email}
let _ = await ApiClient.createUser(req)
ApiClient.res は fetch をラップし、Shared.Types.user[](または Shared.Api.createUserRes)を返すことで、エンドツーエンドの型ラウンドトリップを正直に保つ。
packages/client/vite.config.mjs¶
/api/* を Hono サーバーに転送するプロキシ付きの Vite+ 設定。ProjectFileBuilders.viteConfigWithProxy を介して生成されるため、他のテンプレートで使われる Vite+/Hono コントラクトと一貫性を保つ。
npm スクリプト¶
ルート¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
同じファンアウトを |
|
|
|
|
|
|
packages/server¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
ReScript のコンパイル/ウォッチ/クリーン |
packages/client¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
ReScript のコンパイル/ウォッチ/クリーン |
2日目以降のレシピ¶
Monorepo のセットアップ —— monorepo ワークスペース向けの IDE 側 LSP 設定の Tips
Hono エンドポイントの追加 —— サーバー側に型付きの POST/GET ルートを追加する
Drizzle のセットアップ —— Drizzle スキーマを拡張する(テンプレートにはすでに 1 つ同梱されている)
React コンポーネントの作成 —— クライアント側に新しい React コンポーネントを追加するパターン
OpenAPI ドキュメントの追加 —— Hono ルートから OpenAPI ドキュメントを生成する
プロジェクトを開いた後の ReScript 側のエディタワークフローについては 機能概要 を参照する。
補足¶
ワークスペース宣言の形はパッケージマネージャによって変わる。 pnpm は別ファイルの
pnpm-workspace.yamlを使い、npm、yarn、bun はいずれもルートpackage.jsonのworkspacesフィールドを使う。ウィザードが自動的に正しい方を生成する。npm は
workspace:*を依存指定子として拒否し、リテラルバージョンとして扱う。テンプレートは npm の場合*にフォールバックし、pnpm・yarn・bun ではworkspace:*を使う。concurrentlyは 3 階層で動作する。 ルートのdevは 2 つのワークスペースウォッチャーを並列実行する。各ワークスペースのdevはそれぞれの node/vite ウォッチャーと並んでrescript -wを実行する。内側のconcurrentlyがないと、dev実行中に.resファイルを編集しても再コンパイルされず、古い.res.mjsを何時間も追いかけるはめになる —— 過去のテンプレートバージョンが踏んだ罠である。packages/sharedには独自のrescript -wはない —— ReScript はプロジェクトルートからワークスペースを巡回するため、ルートのres:devが網羅する。パッケージ単位で制御したい場合に備えて、各ワークスペースもres:build/res:dev/res:cleanを公開している。vitest 設定は
DATABASE_URL=:memory:を固定する ので、スモークテストは実際の SQLite ファイルを開こうとしない。コミット済みの.env.exampleは本番形のデフォルト(file:./data/app.db)を示している。Vite+ は 1.0 以前のリリースである。 将来のリリースで動かなくなった場合は、
packages/client/vite.config.mjsのvite-plusを素のviteに置き換え、vpスクリプトをviteに置き換えること。クラシックな Vite 依存はすでにフォールバックとして宣言されている。packages/server/data/、packages/*/dist/、.vite/、.envは gitignore 対象である。db:migrateが生成する SQLite ファイルはバージョン管理の対象外となる。CORS はデフォルトで無効になっている。 Vite+ プロキシが dev 時にリクエストを同一オリジンに保つためである。コメント解除すればすぐ使える
Hono.cors(...)ブロックがpackages/server/src/Server.resの冒頭に置かれている。クライアントとサーバーを別ホスティングでデプロイする前に、オリジンの許可リストを固定すること。