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

__root.tsx
{-$locale}.tsx
sitemap[.]xml.ts
router.tsx
routeTree.gen.ts
styles.css
source.config.ts
wrangler.jsonc
drizzle.config.ts

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 fileResponsibility
{-$locale}/_marketing.tsxBanner, application header/footer, landing pages, blog, legal pages, contact, AI examples, payment success
{-$locale}/_dashboard.tsxAuthenticated sidebar shell and dashboard/account/settings routes
{-$locale}/_docs.tsxFumadocs tree and documentation layout
{-$locale}/auth.tsxLogin, 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

PathContents
src/componentsFeature UI plus ui, home, layout, auth, ai, ai-elements, dashboard, and motion families
src/configSite identity, routes, pricing, footer, social links, and sidebar configuration
src/libAuth, guards, D1/Drizzle, email, payments, storage, SEO, AI providers, and content loaders
src/i18ni18next initialization and locale-aware routing helpers
src/hooks, src/typesShared hooks and TypeScript contracts
messagesEnglish and Chinese UI dictionaries
contentFumadocs docs plus blog and legal MDX
publicVersioned static assets; not the durable upload store
source.config.tsFumadocs collection schemas
drizzle.config.ts, drizzle/Drizzle Kit configuration and reviewed SQLite migrations
wrangler.jsoncWorker 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:

  1. Keep configuration lookup inside a function reached during a request.
  2. Cache a client only after the first request-time initialization when safe.
  3. Never read DB or UPLOADS at module initialization.
  4. Keep authorization and input validation beside each server boundary.

On this page