- Protected routes live under
src/routes/_auth/**, enforced bybeforeLoadin the_authlayout (src/routes/_auth/route.tsx). - Guest-only routes live under
src/routes/_guest/**, enforced bybeforeLoadin the_guestlayout (src/routes/_guest/route.tsx). - Auth-specific route guard behavior and middleware rules are documented in
.agents/auth.md.
Route loaders are isomorphic; they run on both server and client. They cannot directly access server-only APIs.
// Bad: direct server API access
loader: async () => {
const todos = await fs.readFile("todos.json");
return { todos };
};// Good (minimal/valid): call a server function from the loader
loader: async () => {
const todos = await $getTodos({ data: {} });
return { todos };
};Instead of directly calling server functions in loaders, prefer wrapping in TanStack Query for better caching and reusability.
loader: async ({ context }) => {
// Best/Preferred: For read/data-fetching server functions, wrap in TanStack Query
const todos = await context.queryClient.ensureQueryData(todosQueryOptions());
return { todos };
};
// lib/todos/queries.ts
export const todosQueryOptions = () =>
queryOptions({
queryKey: ["todos"],
queryFn: ({ signal }) => $getTodos({ signal }), // TanStack Query calls the server function
});TanStack Start strips any code not referenced by a createServerFn handler from the client build.
- Server-only code (database, fs) is automatically excluded from client bundles
- Only code inside
createServerFnhandlers goes to server bundles - Code outside handlers is included in both bundles
Server functions wrapped in createServerFn can be imported statically. Never use dynamic imports for server-only code in components. Prefix server function names with $ (e.g. $getUser) for easier identification.
// Bad: dynamic import causes bundler issues
const rolesQuery = useQuery({
queryFn: async () => {
const { $listRoles } = await import("@/utils/roles.server");
return $listRoles({ data: {} });
},
});
// Good: static import
import { $listRoles } from "@/utils/roles.server";
const rolesQuery = useQuery({
queryFn: async () => $listRoles({ data: {} }),
});createServerFnwrappers can be imported statically anywhere- Direct server-only code (database clients, fs) must only be imported:
- Inside
createServerFnhandlers - In
*.server.tsfiles
- Inside
- See
.agents/auth.mdfor auth middleware usage, route guards, and session/cookie patterns.