Project Structure
How routes, services, content, and Cloudflare resources fit together in the TanStack template.
The template separates URL structure, reusable UI, server integrations, and
Cloudflare resources. TanStack Router generates the route tree from
src/routes; the application code should never edit that generated tree.
Top-level map
Route hierarchy
src/routes/__root.tsx owns the HTML shell and global providers: SEO defaults,
the theme hydration script, ThemeProvider, Fumadocs RootProvider, toaster,
devtools, and router scripts. It intentionally has no product header or footer;
child layouts decide which chrome belongs around a page.
src/routes/{-$locale}.tsx adds an optional locale segment. English is
unprefixed, while Chinese lives below /zh. Its beforeLoad validates the
segment, redirects /en/* to the canonical unprefixed URL, and synchronizes
i18next before rendering.
The underscore-prefixed route IDs are pathless layouts: they organize UI
without adding _marketing, _dashboard, or _docs to the URL.
| Route file | Responsibility |
|---|---|
{-$locale}/_marketing.tsx | Banner, application header/footer, landing pages, blog, legal pages, contact, AI examples, payment success |
{-$locale}/_dashboard.tsx | Authenticated sidebar shell and dashboard/account/settings routes |
{-$locale}/_docs.tsx | Fumadocs tree and documentation layout |
{-$locale}/auth.tsx | Login, registration, confirmation, and password-reset shell |
Dynamic files such as _marketing/blog/$slug.tsx and
_marketing/legal/$slug.tsx receive the final path segment as params.slug.
The docs catch-all uses _docs/docs/$.tsx.
src/router.tsx creates the router and configures scroll restoration and intent
preloading. It imports src/routeTree.gen.ts, which the router plugin regenerates
from the route files.
Never edit src/routeTree.gen.ts. Add, rename, or remove a file under
src/routes, then let the TanStack Router plugin regenerate the tree.
API routes and server functions
Most API route files expose HTTP methods through server.handlers. The Better
Auth catch-all is deliberately small:
export const Route = createFileRoute("/api/auth/$")({
server: {
handlers: {
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
},
},
})Feature APIs live below src/routes/api: auth, user/admin, contact, payments,
storage, chat, and AI media. Sensitive handlers must check the session again;
layout protection is not an API authorization boundary. The user profile route
shows the intended shape: get the session, validate mutations with Zod, and
return JSON such as { success, data } or { success: false, error } with an
appropriate status.
Not every existing route follows exactly the same validation or response pattern. Treat the authenticated, validated user routes as the direction for new work and normalize older examples as you productize them.
createServerFn is for server logic that components or router hooks can invoke
without inventing an HTTP endpoint. src/lib/auth-guard.ts uses it to read the
request session. The _dashboard layout calls requireAuth in beforeLoad,
and the admin users page calls requireAdmin.
Supporting directories
| Path | Contents |
|---|---|
src/components | Feature UI plus ui, home, layout, auth, ai, ai-elements, dashboard, and motion families |
src/config | Site identity, routes, pricing, footer, social links, and sidebar configuration |
src/lib | Auth, guards, D1/Drizzle, email, payments, storage, SEO, AI providers, and content loaders |
src/i18n | i18next initialization and locale-aware routing helpers |
src/hooks, src/types | Shared hooks and TypeScript contracts |
messages | English and Chinese UI dictionaries |
content | Fumadocs docs plus blog and legal MDX |
public | Versioned static assets; not the durable upload store |
source.config.ts | Fumadocs collection schemas |
drizzle.config.ts, drizzle/ | Drizzle Kit configuration and reviewed SQLite migrations |
wrangler.jsonc | Worker name, runtime compatibility, routes, vars, D1, and R2 bindings |
Both #/* and @/* resolve to src/*; template code primarily uses #/*.
Workers runtime model
D1 DB and R2 UPLOADS are Cloudflare bindings, not connection strings.
Code imports env from cloudflare:workers and reads the binding inside a
request-time operation. The database and auth exports use lazy proxies, while
Creem and Resend use lazy factories/singletons, so Worker secrets and bindings
are not captured during module evaluation.
Use the same pattern for new runtime clients:
- Keep configuration lookup inside a function reached during a request.
- Cache a client only after the first request-time initialization when safe.
- Never read
DBorUPLOADSat module initialization. - Keep authorization and input validation beside each server boundary.
