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 name となり、各ワークスペースパッケージ(@<project>/shared@<project>/server@<project>/client)のプレフィックスになる

Package manager

npm / pnpm / yarn / bun のいずれか。ワークスペース宣言の形、ワークスペースごとのコマンド構文、dev/test のファンアウトスクリプト、ワークスペース依存のバージョン指定子、README のインストール/実行スニペット、CI キャッシュキーをそれぞれ切り替える

Validation library

zodsury。どの packages/server/src/Validation.res バリアントを同梱するかと、サーバーワークスペースに追加する依存を選択する

パッケージマネージャの選択は、インストールコマンドに加えて、設計上重要な 3 点に影響する:

観点

pnpm

yarn

npm

bun

ワークスペース宣言

pnpm-workspace.yaml

workspaces フィールド

workspaces フィールド

workspaces フィールド

ワークスペース単位コマンド

pnpm --filter ./packages/<x> <s>

yarn workspace ./packages/<x> run <s>

npm --workspace packages/<x> run <s>

bun --filter ./packages/<x> <s>

ワークスペース依存バージョン

workspace:*

workspace:*

*(npm は workspace:* を拒否する)

workspace:*

主要な依存

ルート

パッケージ

用途

バージョン

concurrently (dev)

サーバーとクライアントの dev ウォッチャーを並列で実行する

TemplateVersions.CONCURRENTLY

packages/shared

パッケージ

用途

バージョン

rescript

ReScript コンパイラ

TemplateVersions.RESCRIPT

@rescript/core

標準ライブラリ

TemplateVersions.RESCRIPT_CORE

@rescript/runtime

コンパイル済み .res.mjs の import に必要なランタイムスタブ

TemplateVersions.RESCRIPT_RUNTIME

packages/server

パッケージ

用途

バージョン

rescript, @rescript/core, @rescript/runtime

ReScript コンパイラ+ランタイム

TemplateVersions.RESCRIPT / _CORE / _RUNTIME

@<project>/shared

共有型パッケージへのワークスペース依存

workspace:*(npm の場合は *

hono

HTTP フレームワーク

TemplateVersions.HONO

@hono/node-server

Node HTTP アダプタ

TemplateVersions.HONO_NODE_SERVER

zod または sury

バリデーションバックエンド

TemplateVersions.ZOD / TemplateVersions.SURY

@libsql/client

libsql クライアント(SQLite 互換、Turso 対応)

TemplateVersions.LIBSQL_CLIENT

drizzle-orm

型安全な SQL ビルダー

TemplateVersions.DRIZZLE_ORM

drizzle-kit (dev)

マイグレーションジェネレータ

TemplateVersions.DRIZZLE_KIT

vitest (dev)

テストランナー

TemplateVersions.VITEST

@vitest/coverage-v8 (dev)

カバレッジプロバイダ

TemplateVersions.VITEST_COVERAGE_V8

concurrently (dev)

dev 用に rescript -wnode --watch をペアにする

TemplateVersions.CONCURRENTLY

packages/client

パッケージ

用途

バージョン

rescript, @rescript/core, @rescript/runtime

ReScript コンパイラ+ランタイム

TemplateVersions.RESCRIPT / _CORE / _RUNTIME

@rescript/react

React バインディング

TemplateVersions.RESCRIPT_REACT

@<project>/shared

共有型パッケージへのワークスペース依存

workspace:*(npm の場合は *

react / react-dom

React ランタイム

TemplateVersions.REACT / TemplateVersions.REACT_DOM

vite-plus

Vite+ ビルドツール(vite + vitest + 設計済みデフォルト)

TemplateVersions.VITE_PLUS

@voidzero-dev/vite-plus-core

Vite+ ランタイムコア

TemplateVersions.VITE_PLUS_CORE

vite

基盤となる Vite(フォールバック依存として保持)

TemplateVersions.VITE

@vitejs/plugin-react

React fast-refresh プラグイン

TemplateVersions.VITEJS_PLUGIN_REACT

vitest / @vitest/coverage-v8 (dev)

テストランナー+カバレッジ

TemplateVersions.VITEST / TemplateVersions.VITEST_COVERAGE_V8

concurrently (dev)

rescript -wvp dev をペアにする

TemplateVersions.CONCURRENTLY

主要なファイル

packages/shared/src/Types.resApi.res

共有コントラクトを担う 2 つの小さなモジュール:

// packages/shared/src/Types.res
type user = {id: int, name: string, email: string}

// packages/shared/src/Api.res
type createUserReq = {name: string, email: string}
type createUserRes = {id: int, name: string, email: string}

shared パッケージの rescript.jsonnamespace: "Shared" を設定するため、利用側は Shared.Types.userShared.Api.createUserReq のように参照する。2 つのパッケージがたまたま Types を定義してもフラットモジュールの衝突は起きない。

packages/server/src/Server.res

3 つのルート(GET /api/helloGET /api/usersPOST /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.resApiClient.res

App.res は小さなユーザーフォームとリストである。重要な点は Shared.Api.createUserReq の型注釈であり、packages/shared にずれが生じればクライアントとサーバーの 両方 でコンパイルエラーが発生する。これは破壊が起きてほしい瞬間に起きるという、まさに望ましい挙動である:

let req: Shared.Api.createUserReq = {name, email}
let _ = await ApiClient.createUser(req)

ApiClient.resfetch をラップし、Shared.Types.user[](または Shared.Api.createUserRes)を返すことで、エンドツーエンドの型ラウンドトリップを正直に保つ。

packages/client/vite.config.mjs

/api/* を Hono サーバーに転送するプロキシ付きの Vite+ 設定。ProjectFileBuilders.viteConfigWithProxy を介して生成されるため、他のテンプレートで使われる Vite+/Hono コントラクトと一貫性を保つ。

npm スクリプト

ルート

スクリプト

説明

dev

concurrently "<server-dev>" "<client-dev>" —— 両方のウォッチャーを並列で実行する

dev:server

packages/serverdev スクリプトへのワークスペース単位ディスパッチ

dev:client

packages/clientdev スクリプトへのワークスペース単位ディスパッチ

build:client

packages/clientbuild スクリプトへのワークスペース単位ディスパッチ

test

test スクリプトを公開するすべてのワークスペースへファンアウトする(pnpm -r run test / yarn workspaces foreach -A run test / npm --workspaces run test --if-present / bun --filter '*' run test

test:coverage

同じファンアウトを test:coverage 向けに行う

res:build

rescript —— monorepo 全体をワンショットコンパイル

res:dev

rescript -w —— ウォッチモード

res:clean

rescript clean

packages/server

スクリプト

説明

start

node src/ServerMain.res.mjs

dev

concurrently "npm:res:dev" "node --watch src/ServerMain.res.mjs"

test

vitest run

test:coverage

vitest run --coverage

db:generate

drizzle-kit generate

db:migrate

drizzle-kit migrate

res:build / res:dev / res:clean

ReScript のコンパイル/ウォッチ/クリーン

packages/client

スクリプト

説明

dev

concurrently "npm:res:dev" "vp dev" —— ReScript ウォッチと Vite+ dev サーバーをペアにする

build

vp build

preview

vp preview

test

vp test

test:coverage

vp test --coverage

res:build / res:dev / res:clean

ReScript のコンパイル/ウォッチ/クリーン

2日目以降のレシピ

プロジェクトを開いた後の ReScript 側のエディタワークフローについては 機能概要 を参照する。

補足

  • ワークスペース宣言の形はパッケージマネージャによって変わる。 pnpm は別ファイルの pnpm-workspace.yaml を使い、npm、yarn、bun はいずれもルート package.jsonworkspaces フィールドを使う。ウィザードが自動的に正しい方を生成する。

  • 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.mjsvite-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 の冒頭に置かれている。クライアントとサーバーを別ホスティングでデプロイする前に、オリジンの許可リストを固定すること。