Scaffold TanStack Start App
Add a TanStack Start + Vite SSR app to an existing monorepo scaffolded with /dlc-scaffold-monorepo.
Arguments
$1- app name (required). Createsapps/$1/. Example:web
If $1 is empty, ask the user for an app name. Detect @{org} scope from root package.json.
Step 1 - App Config Files
apps/$1/package.json
{
"name": "@{org}/$1",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "bun --bun vite dev",
"build": "bun --bun vite build",
"serve": "bun --bun vite preview",
"start": "bun run .output/server/index.mjs",
"typecheck": "tsc --noEmit",
"fmt": "biome format --write ./src",
"lint": "biome lint --write ./src",
"check": "biome check ./src",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}
apps/$1/biome.jsonc
Use the React app variant (TanStack / Tauri) from /dlc-scaffold-biome.
apps/$1/tsconfig.json
Use the TanStack Start app variant from /dlc-scaffold-tsconfig.
apps/$1/vite.config.ts
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { nitro } from "nitro/vite";
import viteReact from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
enabled: true,
autoSubfolderIndex: true,
crawlLinks: true,
},
}),
nitro({ preset: "bun" }),
viteReact(),
],
});
- Nitro
bunpreset - builds to.output/server/index.mjsfor native Bun execution - Prerendering enabled by default - static routes auto-discovered,
crawlLinksfollows<a>tags to discover more pages - Dynamic param routes (
/$orgSlug) are auto-excluded from prerendering
Step 2 - Directory Structure
apps/$1/src/
├── _routes/ # TanStack Router file-based routes
│ ├── __root.tsx
│ ├── login/
│ ├── auth/
│ └── _authed/
│ ├── _layout.tsx
│ └── $orgSlug/
├── core/
│ ├── AGENTS.md
│ ├── CLAUDE.md
│ ├── index.ts
│ ├── logger.ts
│ ├── api/
│ │ ├── {domain}.api.ts # createServerFn wrappers
│ │ ├── clients/
│ │ │ ├── {domain}.ts # QueryClient subclass
│ │ │ └── index.ts # createApiClients()
│ │ ├── hooks/
│ │ │ └── {domain}.hook.ts
│ │ └── util/
│ │ └── logging.middleware.ts
│ └── lib/
│ ├── query-client.ts
│ ├── result.ts
│ └── result.test.ts
├── modules/
│ ├── AGENTS.md
│ ├── CLAUDE.md
│ └── {module-name}/
│ ├── components/
│ ├── types/
│ └── utils/
├── components/
│ ├── ui/
│ └── layouts/authed/
├── hooks/
├── lib/
│ ├── auth/
│ ├── db.ts
│ ├── api.ts
│ ├── storage.ts
│ └── utils.ts
├── test/
├── router.tsx
├── app.css
└── routeTree.gen.ts
Step 3 - Key Patterns
Server Function Pattern
// core/api/colors.api.ts
import { createServerFn } from "@tanstack/react-start";
import { loggingMiddleware } from "./util/logging.middleware";
export const getColors = createServerFn({ method: "GET" })
.middleware([loggingMiddleware])
.handler(async () => {
// DB access or service call here
});
Logging Middleware
// core/api/util/logging.middleware.ts
import { createMiddleware } from "@tanstack/react-start";
export const loggingMiddleware = createMiddleware().server(async ({ next }) => {
const start = performance.now();
const result = await next();
const duration = performance.now() - start;
console.info(`[${duration.toFixed(0)}ms]`, result);
return result;
});
Client Class Pattern
// core/api/clients/colors.ts
import { QueryClient } from "../lib/query-client";
import { getColors, createColor } from "../colors.api";
export class ColorsClient extends QueryClient<"colors"> {
constructor(queryClient: TanStackQueryClient) {
super(queryClient, "colors");
}
list = this.query("list", {
queryFn: () => getColors(),
});
create = this.mutation("create", {
mutationFn: (input: CreateColorInput) => createColor({ data: input }),
onSuccess: () => this.list.refresh(),
});
}
createApiClients() Factory
// core/api/clients/index.ts
export function createApiClients(queryClient: TanStackQueryClient) {
return {
colors: new ColorsClient(queryClient),
};
}
export type ApiClients = ReturnType<typeof createApiClients>;
Router Context
// router.tsx - context shape
interface RouterContext {
queryClient: TanStackQueryClient;
session: Session | null;
organisation: Organisation | null;
clients: ApiClients;
}
Data Flow
Route beforeLoad → createServerFn (server) → DB/service
↓
Route context (queryClient, session, org, clients)
↓
Component → useQuery / useMutation via client
Prerendering Configuration
Default config in vite.config.ts prerenders all static routes. Customise via tanstackStart() options:
// vite.config.ts - full prerender options reference
tanstackStart({
prerender: {
enabled: true,
autoSubfolderIndex: true, // /page → /page/index.html
autoStaticPathsDiscovery: true, // auto-discover static routes (default)
crawlLinks: true, // follow <a> tags to discover more pages
concurrency: 14, // parallel prerender jobs
filter: ({ path }) => !path.startsWith("/api"),
retryCount: 2,
retryDelay: 1000,
maxRedirects: 5,
failOnError: true,
onSuccess: ({ page }) => console.log(`Prerendered ${page.path}`),
},
})
| Option | Type | Default | Purpose |
|---|---|---|---|
enabled |
boolean |
false |
Activate prerendering |
autoSubfolderIndex |
boolean |
false |
Output /page/index.html instead of /page.html |
autoStaticPathsDiscovery |
boolean |
true |
Auto-discover routes without params |
crawlLinks |
boolean |
false |
Follow links to discover additional pages |
concurrency |
number |
- | Parallel prerender jobs |
filter |
(page) => boolean |
- | Include/exclude specific paths |
failOnError |
boolean |
false |
Halt build on prerender errors |
Dynamic param routes (e.g. /$orgSlug) are excluded from auto-discovery but can be prerendered if linked from other pages when crawlLinks is enabled.
Step 4 - AGENTS.md Files
apps/$1/AGENTS.md
Contains:
- Stack: TanStack Start, Vite, TanStack Router, TanStack Query, Tailwind CSS v4, Nitro (bun preset)
- Dev commands (
bun run devon port 3000) - Build/deploy:
bun --bun vite build→.output/→bun run .output/server/index.mjs - Static routes prerendered at build time (crawlLinks enabled)
- Full
src/directory tree - TDD workflow
- Key patterns: routing, auth guard via
_authed/, data flow, prerendering - How to add new pages
apps/$1/src/core/AGENTS.md
Contains:
- Overview: "Data layer - server functions, API clients, and shared utilities"
- File structure tree
- Naming conventions table
- Architecture: server function → client class → hook → component
- Prerendering: server functions run at build time - handle missing auth/session gracefully for prerendered routes
- Rules 1-4 (see below)
- "Adding a New Domain" checklist (6 steps):
- Create
core/api/{domain}.api.tswith server functions - Create
core/api/clients/{domain}.tsextending QueryClient - Register in
core/api/clients/index.ts - Create
core/api/hooks/{domain}.hook.ts - Add types to
core/types/{domain}.types.tsif needed - Write tests:
{domain}.test.ts
- Create
apps/$1/src/modules/AGENTS.md
Contains:
- Purpose: "Feature modules - self-contained UI features"
- Structure template per module
- 5 rules: no barrel exports, colocate tests, import from core/ not between modules, single responsibility, keep components small
All CLAUDE.md files
Every directory with an AGENTS.md also gets CLAUDE.md containing only: See @AGENTS.md
Step 5 - TanStack Start Rules
In addition to the 8 shared rules from /dlc-scaffold-monorepo:
- All DB access via server functions - use
createServerFn+ services, never query DB from client - Never call DB from client code - all data flows through server functions
- Middleware for cross-cutting concerns - logging, timing, auth checks via
createMiddleware - Nitro
bunpreset for deployment - usenitro({ preset: 'bun' })in vite.config.ts; build outputs to.output/server/index.mjs; start withbun run .output/server/index.mjs - Prerender static routes - prerendering enabled by default; dynamic param routes auto-excluded; use
filterto exclude additional paths;crawlLinksdiscovers linked pages
Step 6 - Verify
bun install- resolves workspace depsbun run typecheck- zero errorsbun run check- biome passesbun run test- all tests passbun run dev- TanStack Start dev server starts on port 3000bun --bun vite build- produces.output/directory with server bundlebun run .output/server/index.mjs- production server starts on port 3000- Check
.output/public/for prerendered HTML files (e.g.login/index.html) - Verify AGENTS.md files at: app root,
src/core/,src/modules/