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>
85 lines
3.5 KiB
Markdown
85 lines
3.5 KiB
Markdown
# 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 |
|