Full-Stack (single package)

Hono Vite+/React Drizzle Validation Shared Types

1 つの package.json で完結するフルスタック ReScript アプリケーション。サーバー(SQLite 上の Hono + Drizzle)とクライアント(Vite+/React)が src/server/src/client/ 配下に並んで配置され、3 つ目のソースルート src/shared/ に両側が import する型を置く。共有型と単一プロセスの dev ループを望み、Monorepo (Hono + React) テンプレートが要求するワークスペースツーリングのコストを払いたくない場合に、このテンプレートを選ぶ。

ウィザードはさらに API strategy という軸を提示する: REST(手書きの Hono ルートとfetch クライアント)か GraphQL(/graphql にマウントされる graphql-yoga とクライアント側の rescript-relay)。共有インフラ —— Drizzle スキーマ、libsql クライアント、vitest セットアップ、Vite+ プロキシ、Shared.res —— はどちらのモードでも同一である。

生成内容 (REST)

my-app/
├── rescript.json                       # sources: src/shared, src/server, src/client (subdirs)
├── package.json                        # ESM, bundles server + client deps
├── index.html                          # Vite entry — loads /src/client/ClientMain.res.mjs
├── vite.config.mjs                     # Vite+ + react plugin + /api proxy
├── 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/
│   ├── shared/Shared.res               # nested modules: Shared.Types.* / Shared.Api.*
│   ├── server/
│   │   ├── ServerMain.res              # entry — Server.start()
│   │   ├── Server.res                  # Hono app + GET /api/health + Routes.Users.register
│   │   ├── Routes.res                  # nested: module Users { let register = app => ... }
│   │   ├── Schema.res                  # Drizzle SQLite schema (users)
│   │   ├── Db.res                      # libsql client + helpers
│   │   ├── Validation.res              # zod or sury — selected in the wizard
│   │   ├── Hono.res / HonoNodeServer.res
│   │   └── __tests__/Server.test.mjs   # vitest: app.request("/api/health") returns 200
│   └── client/
│       ├── ClientMain.res              # React root render
│       ├── App.res                     # users form + list (uses Shared.Types.user)
│       ├── ApiClient.res               # fetch wrapper, types pinned to Shared.Api.*
│       └── __tests__/Api.test.mjs
├── README.md                           # Architecture + Shared Types + Database + Layout sections
├── LICENSE                             # MIT, holder = project name
├── .nvmrc                              # Node 24
├── .gitignore                          # adds data/, dist/, .vite/, drizzle/, .env
├── .editorconfig
└── .github/
    ├── dependabot.yml
    └── workflows/ci.yml                # install + vitest

GraphQL バリアントでは src/server/Yoga.ressrc/server/GraphqlSchema.ressrc/server/Resolvers.ressrc/server/schema.graphqlsrc/client/RelayEnvironment.ressrc/client/UsersListQuery.resrelay.config.js が追加され、src/client/__generated__/ が gitignore され、graphqlgraphql-yogarescript-relayrelay-compiler が宣言され、relay/relay:watch のスクリプトペアと、dev に追加の npm:relay:watch トークンが追加される。

ウィザードオプション

オプション

効果

Project name

npm の name、LICENSE の保持者、src/client/App.res<h1> テキストになる

Package manager

npm / pnpm / yarn / bun のいずれか。packageManager Corepack フィールド、README のインストール/実行コマンド、CI キャッシュキーを切り替える

Validation library

zodsury。どの src/server/Validation.res バリアントを同梱するかを選択し、対応する依存を追加する

API strategy

REST ↔ GraphQL。サーバー/クライアントの形(手書きの Hono ルート + fetch か、graphql-yoga + rescript-relay か)を選び、依存・スクリプト・rescript.json の ppx-flags をそれに合わせて調整する

主要な依存 (REST)

パッケージ

用途

バージョン

rescript, @rescript/core, @rescript/runtime

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

TemplateVersions.RESCRIPT / _CORE / _RUNTIME

@rescript/react

React バインディング

TemplateVersions.RESCRIPT_REACT

react, react-dom

React ランタイム

TemplateVersions.REACT / TemplateVersions.REACT_DOM

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

vite-plus, @voidzero-dev/vite-plus-core, vite

Vite+ ビルドツール+クラシック Vite のフォールバック

TemplateVersions.VITE_PLUS / _CORE / 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 -wnode --watchvp dev を同時に実行する

TemplateVersions.CONCURRENTLY

GraphQL バリアントで追加されるもの

パッケージ

用途

バージョン

graphql

リファレンス実装。graphql-yogarescript-relay の peer dep

TemplateVersions.GRAPHQL

graphql-yoga

Yoga.res を介して Hono にマウントする GraphQL サーバー

TemplateVersions.GRAPHQL_YOGA

rescript-relay

Relay ランタイム向けの ReScript バインディング+ppx

TemplateVersions.RESCRIPT_RELAY

relay-compiler (dev)

schema.graphql から型付きの %relay() 成果物を生成する

TemplateVersions.RELAY_COMPILER

主要なファイル

src/shared/Shared.res

module Types = {
  type user = {id: int, name: string, email: string}
}

module Api = {
  type createUserReq = {name: string, email: string}
  type createUserRes = {id: int, name: string, email: string}
}

ネストされたモジュール構造により、利用側は Shared.Types.userShared.Api.createUserReq のように型へ到達できる。grep しやすく、リファクタリングも容易で、import の曲芸は不要である。フィールドを編集すると、src/server/src/client/ の両方が整合するまで再コンパイルされる。

src/server/Server.res (REST)

Hono アプリの骨組み。ヘルスチェックルートはインラインに置かれ、リソース別のルートは src/server/Routes.res に配置され、小さな規約を介して自分自身を登録する:

let app = Hono.createApp()

app->Hono.onError((err, ctx) => {
  Console.error(err)
  ctx->Hono.status(500)->Hono.json({"error": "Internal Server Error"})
})

app->Hono.get("/api/health", ctx => ctx->Hono.json({"status": "ok"}))
Routes.Users.register(app)

let start = () => {
  HonoNodeServer.serve({fetch: app->HonoNodeServer.honoFetch, port: 3000})
  Console.log("Server on http://localhost:3000 — try /api/health")
}

新しいリソースを追加するには、Routes.res に兄弟モジュール(module Posts = { let register = app => ... })を追加し、ここに Routes.Posts.register(app) の呼び出しを 1 行追加する。

src/server/Routes.res

実際のハンドラロジックを保持する。同梱の Users グループは Drizzle のフルラウンドトリップを実行する(select → 行を返す、insert → バリデーション失敗時は 400、成功時は 201 と挿入された行を返す)。

src/server/Validation.res

parseCreateUserReq: JSON.t => result<createUserReq, string> —— POST /api/users のランタイムコントラクト。シグネチャは zod / sury のどちらのバリアントでも同一なので、呼び出し側がどちらのライブラリが採用されたかで分岐する必要はない。

src/server/ServerMain.res

中身は Server.start() の 1 行のみ。この分割により、テストが Server.res.mjs を import する前に vitest.setup.mjsDATABASE_URL=:memory: を固定でき、ポートにバインドせずに app.request("/api/health") を呼び出せる:

// vitest.setup.mjs
process.env.DATABASE_URL = ":memory:";

src/client/App.res

小さなユーザーフォームとリスト。ワイヤフォーマット型が明示されているため、src/shared/Shared.res のずれは即座に現れる:

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

src/client/ApiClient.res

薄い fetch ラッパ。すべてのルートが /api/* 配下にあるので、vite.config.mjs のVite+ プロキシが dev 時に Hono サーバーへ転送できる(CORS は不要)。本番環境では同じサーバーが両方をホストする。

vite.config.mjs

ProjectFileBuilders.viteConfigWithProxy を介して生成される —— Vite+ + React fast-refresh + Hono サーバーをターゲットとする /api/* プロキシ。デプロイを 2 つのオリジンに分割する場合は、プロキシのターゲットを編集すること。

GraphQL バリアント: src/server/GraphqlSchema.res, Resolvers.res, schema.graphql

GraphQL スキーマは同期を保つべき 2 箇所に存在する: src/server/schema.graphql の SDL(ビルド時に Relay コンパイラが読む)と、src/server/GraphqlSchema.res 内にインライン展開された typeDefs 文字列(ランタイムに graphql-yoga が読む)。リゾルバは src/server/Resolvers.res 配下にネストされたモジュール(例えば module Users)として整理されている。

GraphQL バリアント: src/client/RelayEnvironment.res, UsersListQuery.res

RelayEnvironment.res は Relay クライアントを設定する。クエリは %relay() タグ(例: module UsersListQuery = %relay( ... ))として書かれる。クエリを編集した後は relay-compiler --watch を実行して src/client/__generated__/<Query>_graphql.res を再生成する。これらの生成ファイルは gitignore 対象である。

npm スクリプト (REST)

スクリプト

説明

dev

concurrently "npm:res:dev" "npm:dev:server" "npm:dev:client" —— 3 つのウォッチャーを並列で実行する

dev:server

node --watch src/server/ServerMain.res.mjs

dev:client

vp dev —— Vite+ dev サーバー(Hono バックエンドへの /api プロキシ付き)

build

vp build —— 本番向けにクライアントをバンドルする

preview

vp preview —— 本番ビルドをプレビューする

test

vitest run —— サーバーとクライアントのスモークテストをカバーする

test:coverage

vitest run --coverage

db:generate

drizzle-kit generate —— マイグレーション SQL を生成する

db:migrate

drizzle-kit migrate —— 未適用のマイグレーションを反映する

res:build

rescript —— src/sharedsrc/serversrc/client を横断するワンショットコンパイル

res:dev

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

res:clean

rescript clean

npm スクリプト(GraphQL で追加されるもの)

スクリプト

説明

dev

concurrently のファンアウトに "npm:relay:watch" を追加する

relay

relay-compiler —— 型付きの %relay() 成果物を 1 度生成する

relay:watch

relay-compiler --watch —— スキーマ/クエリ編集に応じて再生成し続ける

2日目以降のレシピ

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

補足

  • 1 つの package.json で世界を覆う。 サーバー・クライアント・共有の各ルートは、3 つの sources エントリを持つ単一の rescript.json を介してコンパイルされる。npm install を 1 回実行するだけで、すべての依存関係がインストールされる。

  • devconcurrently 経由で 3 つ(GraphQL の場合は 4 つ)のプロセスを起動する。 内訳は rescript -wnode --watch src/server/ServerMain.res.mjsvp dev、そして(GraphQL のみ)relay-compiler --watch である。dev の初回実行時にサーバー起動が失敗する場合、rescript -w がまだ最初の .res.mjs を生成していないことを意味する —— 1 サイクル待つか dev を再実行する。

  • Vite+ は /api/*(GraphQL バリアントでは /graphql も)を Hono サーバーへプロキシし、dev 時にブラウザリクエストを同一オリジンに保つ。CORS はデフォルトで無効である。クライアントとサーバーを別ホスティングでデプロイする前に、src/server/Server.res 冒頭の Hono.cors(...) ブロックのコメントを解除し、オリジンの許可リストを固定すること。

  • Shared.ressrc/shared/ に置かれている —— src/shared/Shared.res 配下のネストモジュールが依存方向を明確に保つためである。両側が Shared に依存し、Shared はどちらにも依存しない。record フィールドをリネームすると、両側が再コンパイルされる。

  • vitest.setup.mjsDATABASE_URL=:memory: を固定する —— テストが Server.res.mjs を import する前に実行される。コミット済みの .env.example は本番形のデフォルト(file:./data/app.db)を示す。

  • data/dist/.vite/drizzle/.env(および GraphQL バリアントでは src/client/__generated__/)は gitignore 対象である。

  • GraphQL は 1 度きりのブートストラップ手順を含む: 初回ビルドの前に pnpm relay(または npm run relay)を 1 度は実行して、src/client/__generated__/ を生成しておく必要がある。dev スクリプトは relay:watch を起動し続けるので、初回サイクル以降は意識する必要がない。

  • Vite+ は 1.0 以前のリリースである。 リリースで動かなくなった場合は、vite.config.mjsvite-plusvite に置き換え、vp スクリプトを vite に置き換えること。クラシックな Vite 依存はすでにフォールバックとして宣言されている。