Full-Stack (single package)¶
Hono Vite+/React Drizzle Validation Shared Types
A full-stack ReScript application in one package.json. The server (Hono + Drizzle on SQLite) and client (Vite+/React) live side by side under src/server/ and src/client/, and a third source root, src/shared/, holds types both sides import. Pick this template when you want shared types and a single-process dev loop without paying the workspace tooling tax that the Monorepo (Hono + React) template charges.
The wizard exposes an additional API strategy dimension: REST (handwritten Hono routes plus a fetch client) or GraphQL (graphql-yoga mounted on /graphql plus rescript-relay on the client). The shared infrastructure — Drizzle schema, libsql client, vitest setup, Vite+ proxy, Shared.res — is identical in both modes.
What You Get (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
The GraphQL variant adds src/server/Yoga.res, src/server/GraphqlSchema.res, src/server/Resolvers.res, src/server/schema.graphql, src/client/RelayEnvironment.res, src/client/UsersListQuery.res, and relay.config.js; gitignores src/client/__generated__/; declares graphql, graphql-yoga, rescript-relay, and relay-compiler; and adds a relay/relay:watch script pair plus an extra npm:relay:watch token in dev.
Wizard Options¶
Option |
Effect |
|---|---|
Project name |
Becomes the npm |
Package manager |
npm / pnpm / yarn / bun. Drives the |
Validation library |
|
API strategy |
REST ↔ GraphQL. Selects the server/client shape (handwritten Hono routes + fetch vs. graphql-yoga + rescript-relay) and adjusts dependencies, scripts, and the rescript.json |
Key Dependencies (REST)¶
Package |
Purpose |
Version |
|---|---|---|
|
ReScript compiler + runtime |
|
|
React bindings |
|
|
React runtime |
|
|
HTTP framework |
|
|
Node HTTP adapter |
|
|
Validation backend |
|
|
libsql client (SQLite-compatible, Turso-ready) |
|
|
Type-safe SQL builder |
|
|
Migration generator |
|
|
Vite+ build tool + classic-Vite fallback |
|
|
React fast-refresh |
|
|
Test runner + coverage |
|
|
Runs |
|
GraphQL variant adds¶
Package |
Purpose |
Version |
|---|---|---|
|
Reference implementation, peer dep of |
|
|
GraphQL server mounted on Hono via |
|
|
ReScript bindings + ppx for the Relay runtime |
|
|
Generates typed |
|
Key Files¶
src/server/Server.res (REST)¶
Hono app skeleton. Health route lives inline; the per-resource routes live in src/server/Routes.res and register themselves through a tiny convention:
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")
}
Add a new resource by adding a sibling module to Routes.res (module Posts = { let register = app => ... }) and a single Routes.Posts.register(app) call here.
src/server/Routes.res¶
Holds the actual handler logic. The shipped Users group does the full Drizzle round trip (select → return rows; insert → 400 on validation failure or 201 + the inserted row).
src/server/Validation.res¶
parseCreateUserReq: JSON.t => result<createUserReq, string> — the runtime contract for POST /api/users. The signature is identical between the zod and sury variants so callers do not branch on which library shipped.
src/server/ServerMain.res¶
A single line: Server.start(). The split lets vitest.setup.mjs pin DATABASE_URL=:memory: before the test imports Server.res.mjs to call app.request("/api/health") without binding a port:
// vitest.setup.mjs
process.env.DATABASE_URL = ":memory:";
src/client/App.res¶
A small users form + list. The wire-format types are explicit, so any drift in src/shared/Shared.res shows up immediately:
let req: Shared.Api.createUserReq = {name, email}
let _ = await ApiClient.createUser(req)
src/client/ApiClient.res¶
A thin fetch wrapper. Everything routes under /api/* so the Vite+ proxy in vite.config.mjs can forward to the Hono server in dev (no CORS needed); in production the same server hosts both.
vite.config.mjs¶
Generated through ProjectFileBuilders.viteConfigWithProxy — Vite+ + React fast-refresh + a /api/* proxy targeting the Hono server. Edit the proxy target if you split the deployment across two origins.
GraphQL variant: src/server/GraphqlSchema.res, Resolvers.res, schema.graphql¶
The GraphQL schema lives in two places that must stay in sync: SDL in src/server/schema.graphql (read by the Relay compiler at build time) and the inlined typeDefs string in src/server/GraphqlSchema.res (read by graphql-yoga at runtime). Resolvers are organized into nested modules under src/server/Resolvers.res (e.g. module Users).
GraphQL variant: src/client/RelayEnvironment.res, UsersListQuery.res¶
RelayEnvironment.res configures the Relay client; queries live in %relay() tags (e.g. module UsersListQuery = %relay( ... )). Run relay-compiler --watch to regenerate src/client/__generated__/<Query>_graphql.res after editing a query — those generated files are gitignored.
npm Scripts (REST)¶
Script |
Description |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
npm Scripts (GraphQL adds)¶
Script |
Description |
|---|---|
|
adds |
|
|
|
|
Day-Two Recipes¶
Add a Hono Endpoint — add a typed POST/GET route under
src/server/Routes.resAdd a GraphQL Resolver — add a resolver and matching
%relay()query (GraphQL variant)Set Up Drizzle — deepen the Drizzle schema (
src/server/Schema.res)Create a React Component — add a new client-side React component
Add OpenAPI Docs — generate OpenAPI docs from Hono + zod schemas (REST variant)
For ReScript-side editor workflows once the project is open, see the Feature Overview.
Notes¶
One
package.jsoncovers the world. The server, client, and shared roots compile through a singlerescript.jsonwith threesourcesentries; runningnpm installonce installs all dependencies for everyone.devboots three (or four, with GraphQL) processes viaconcurrently. They are:rescript -w,node --watch src/server/ServerMain.res.mjs,vp dev, and (GraphQL only)relay-compiler --watch. If the server fails to start the first timedevruns, that meansrescript -whas not produced its first.res.mjsyet — wait one cycle or rerundev.Vite+ proxies
/api/*(and/graphqlin the GraphQL variant) to the Hono server, keeping browser requests same-origin in dev. CORS is off by default; uncomment theHono.cors(...)block at the top ofsrc/server/Server.resand pin an origin allowlist before deploying separately-hosted client and server.Shared.reslives insrc/shared/because nested modules undersrc/shared/Shared.reskeep the dependency direction obvious — both sides depend onShared;Shareddepends on neither. Renaming a record field forces both sides to recompile.vitest.setup.mjspinsDATABASE_URL=:memory:before any test importsServer.res.mjs. The committed.env.exampleshows the production-shape default (file:./data/app.db).data/,dist/,.vite/,drizzle/,.env(andsrc/client/__generated__/in the GraphQL variant) are gitignored.GraphQL ships with a one-time bootstrap step:
pnpm relay(ornpm run relay) must run at least once before the first build to populatesrc/client/__generated__/. Thedevscript keepsrelay:watchrunning so this is invisible after the first cycle.Vite+ is pre-1.0. If a release breaks, swap
vite-plusforviteinvite.config.mjsand replace thevpscripts withvite. The classic Vite dep is already declared as a fallback.