Template
Initial commit: Bun · Hono · React SPA · Postgres/Drizzle template
The starting point for an app with a React frontend. Same server, env, database and API skeleton as template-htmx — the two differ only in what is served at / — with a React 19 SPA, Tailwind v4 and shadcn/ui on top. Carried from a working project rather than designed in the abstract, and carrying the lessons that cost it time: - The dev script states NODE_ENV=development rather than inheriting it. React resolves its runtime against NODE_ENV: Bun emits jsxDEV() in development and React's production jsx-dev-runtime defines jsxDEV as void 0. Inherit production while transforming for development and the app dies at first paint. Verified: the dev bundle resolves the development runtime and contains no void-0 stub. - Asset URLs in components are relative and <base href="/"> is in the HTML, so a path-prefix proxy can rewrite one attribute and every asset follows. A test enforces it. - Routing is in the hash, because the app may be mounted under a prefix it only learns at runtime and a hash means the same thing at either mount. - index.html holds __PUBLIC_URL__ and gen-index.ts substitutes it, because Bun's HTML bundler fails the build on a root-relative favicon href. - APP_ENV failing closed, env files before the ambient environment, the database enforced by name, one Postgres client per process — as in the htmx template. The database has no tables. The App is a shell with one tab and no features, named as a placeholder. Renaming `app` is the first step and the README lists every place it appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# template-react
|
||||
|
||||
A starting point for an app. **Bun · Hono · React 19 SPA · Tailwind v4 · shadcn/ui · Postgres with
|
||||
Drizzle.** A JSON API at `/api` and a React app at `/`, over one query layer.
|
||||
|
||||
## 1. Make it yours
|
||||
|
||||
Everything project-specific is called `app`. Rename it once, before the first commit:
|
||||
|
||||
| Find | Replace with | Where |
|
||||
|---|---|---|
|
||||
| `APP_ENV` | `MYPROJECT_ENV` | `src/env-file.ts`, `package.json` scripts, `test-setup.ts`, `src/databases/app_db/src/env.ts` |
|
||||
| `appEnv` | `myprojectEnv` | `src/env-file.ts` and its importers |
|
||||
| `appdb` / `app_db` | `myprojectdb` / `myproject_db` | `src/databases/`, `tsconfig.json` paths, `package.json` scripts |
|
||||
| `app` / `app_dev` (database names) | `myproject` / `myproject_dev` | `src/databases/app_db/src/env.ts`, `.env.example` |
|
||||
| `"name": "app"` | your package name | `package.json` |
|
||||
| `App` | your brand | `src/apps/frontend/App.tsx`, `index.html` `<title>` |
|
||||
| `3000` | a port of your own | `.env.example`, `src/server.ts` |
|
||||
|
||||
A case-sensitive search for `app`, `App` and `APP` finds all of it. Thirty seconds now, a chore later.
|
||||
|
||||
## 2. Setup
|
||||
|
||||
```bash
|
||||
bun install
|
||||
cp .env.example .env # BUN_PORT, PUBLIC_URL, POSTGRES_URL_DEV
|
||||
createdb app && createdb app_dev # or however you make databases
|
||||
bun run db:push # applies the schema — no tables yet, by design
|
||||
bun dev
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
```bash
|
||||
bun dev # development on app_dev, with HMR
|
||||
bun start # production on app
|
||||
bun test
|
||||
bun run build # bundle the SPA to dist/
|
||||
bun run gen:index # index.gen.html from index.html — also run by predev/prestart/prebuild
|
||||
bun run db:push # schema -> app_dev; APP_ENV=production bun run db:push for production
|
||||
```
|
||||
|
||||
## What is served
|
||||
|
||||
| Path | |
|
||||
|---|---|
|
||||
| `/` and every unmatched path | the SPA — `src/apps/frontend` |
|
||||
| `/api/*` | the JSON API — `src/servers/api` |
|
||||
| everything in `public/` | from the root |
|
||||
|
||||
## Configuration, and the rules behind it
|
||||
|
||||
**`APP_ENV`, never `NODE_ENV`.** It must be `production` or `development` and throws when unset.
|
||||
Some hosts export `NODE_ENV=production` into every process they spawn, so anything keyed on it
|
||||
silently treats development as production.
|
||||
|
||||
**`NODE_ENV` is still set by both scripts, and must stay explicit.** React resolves its runtime
|
||||
against it: Bun's bundler emits `jsxDEV()` calls in development, and React's *production*
|
||||
`jsx-dev-runtime` defines `jsxDEV` as `void 0`. Inherit production while transforming for development
|
||||
and the app dies with "jsxDEV is not a function".
|
||||
|
||||
**Env files are read with the ambient environment last:** `.env.production.local` (production only)
|
||||
→ `.env.local` → `.env` → `process.env`.
|
||||
|
||||
**The database follows from `APP_ENV`** and is enforced before a connection opens; tests cannot reach
|
||||
production data.
|
||||
|
||||
**`PUBLIC_URL`** is substituted into `index.html` by `scripts/gen-index.ts` because Bun's HTML bundler
|
||||
fails the build on a root-relative favicon `href`. It must be the public origin in production.
|
||||
|
||||
## The placeholder
|
||||
|
||||
`src/apps/frontend/App.tsx` is a shell with one tab and no features. Add the first screen as a feature
|
||||
folder — `CONVENTIONS.md` describes the shape. The database has **no tables**; `docs/DATABASE.md`
|
||||
describes adding the first.
|
||||
|
||||
## Documentation
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `CLAUDE.md` | The rules an agent needs before touching anything |
|
||||
| `CONVENTIONS.md` | React patterns — feature folders, manager hooks — and the project layout |
|
||||
| `docs/DATABASE.md` | Drizzle structure, `push` not migrations, `check()` vs open `text` |
|
||||
| `docs/SURFACES.md` | The API and the app over one query layer |
|
||||
Reference in New Issue
Block a user