Standards
ReferenceNormative
TanStack Start
The SSR half of the router choice — routes are adapters, server functions are facade exports, and the validation schema is shared with the form.
Verified 2026-07-31 · Against
@tanstack/react-start,@tanstack/react-router,@tanstack/react-form, Vite 7, Node 22.
The framework choice
| Situation | Choice |
|---|---|
| No SSR needed | Vite + TanStack Router. SPA, simplest build, fastest dev loop. |
| SSR needed | TanStack Start. Same router, same file conventions, adds server functions and server rendering. |
| Ever | Never Next.js. Standing decision, with one narrow exception. |
Migrating Vite + Router → Start is incremental, which is exactly why the SPA default costs nothing. Both paths are Vite underneath, so config, plugins and test setup transfer wholly between them.
Routes are adapters, nothing more
A route file resolves params, calls a feature facade, and renders. No data access, no business rule, no direct adapter import.
Enforce it — the client/server rules only mean something if routes obey them.
- Flat file-based routing (
admin.users.index.tsx,api.connect.$provider.callback.ts) keeps the URL readable fromls. Prefer it over deep nesting. - API and handler routes are
api.*.ts— a visible split from page routes. - The generated route tree (
routeTree.gen.ts) is the one file exempt from the no-cycles rule. Never edit it; never lint it. - Type-safe params and search are the reason to use this router at all — do not reintroduce stringly-typed navigation next to it.
Server functions
- A server function is a facade export, not an ad-hoc import. Client-callable
ones go in the feature's
server.ts; server-to-server composition goes inserver-only.ts. Mixing them is how server code lands in a client bundle. - Suffix server modules
*.server.ts. The boundary is then visible in the filename, before any tooling runs, and grep is enough to audit it. - Secrets live in a server-only config module that no client-reachable layer may import. Ship the client only an explicit app-config object served by the root route — allowlist, not filter. See secrets.
- Add a script that verifies these boundaries independently of the bundler. Bundle-level leaks are found late and cost a rebuild each time.
Data and forms
@tanstack/react-formfor forms;@tanstack/react-routerloaders — or Query where a cache is genuinely needed — for reads.- Server state belongs to the feature's
api/facade layer, never to a global service singleton. - The validation schema is shared between the server function and the form. One definition, both sides. A second copy diverges within weeks.
Traps
ssr: falseis not a plan. Decide SSR up front: it drives the facade split, because a client bundle boundary only exists when there is a server.- Importing a server function into
domain/→ the rule that domain has no framework or I/O exists to catch exactly this. - Treating route files as components' home → routes accumulate logic and
become untestable. The component lives in the feature's
ui/; the route imports it viaclient.ts. - Node version drift — pin
enginesand the package manager version. Start's build is sensitive to both.