Build SSR applications with TanStack Start - server functions, file-based routing, and data loading patterns. Use this skill when working on the lexico web application.
This skill covers building server-side rendered (SSR) applications with TanStack Start, including server functions, file-based routing, data loading, and cookie-based authentication.
The lexico application uses TanStack Start for:
For comprehensive patterns and examples, see applications/lexico/AGENTS.md.
applications/lexico/
src/
routes/ # File-based routes
__root.tsx # Root layout
index.tsx # Home page (/)
search.tsx # Search page (/search)
word.$id.tsx # Dynamic route (/word/:id)
components/ # React components
lib/ # Utilities
supabase.client.ts # Client-side Supabase
supabase.server.ts # Server-side Supabase
server/ # Server-only code
functions/ # Server functions
Routes are defined by file names in src/routes/:
index.tsx → /search.tsx → /searchword.$id.tsx → /word/:idauth/callback.tsx → /auth/callbackEach route exports configuration:
// src/routes/word.$id.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/word/$id")({
// Data loading
loader: async ({ params }) => {
const word = await fetchWord(params.id);
return { word };
},
// Error handling
errorComponent: ({ error }) => <div>Error: {error.message}</div>,
// Pending state
pendingComponent: () => <div>Loading...</div>,
// Component
component: WordPage,
});
function WordPage() {
const { word } = Route.useLoaderData();
return <div>{word.latin}</div>;
}
Use $ prefix for dynamic segments:
// src/routes/word.$id.tsx
function WordPage() {
const { id } = Route.useParams(); // Type-safe params
// ...
}
Access query parameters with type safety:
const Route = createFileRoute("/search")({
validateSearch: (search: Record<string, unknown>) => {
return {
q: (search.q as string) || "",
page: Number(search.page) || 1,
};
},
});
function SearchPage() {
const { q, page } = Route.useSearch(); // Type-safe search params
// ...
}
Server functions run only on the server and can access secrets, databases, etc.
// app/server/functions/get-word.ts
import { createServerFn } from "@tanstack/start";
import { createServerClient } from "@/lib/supabase.server";
export const getWord = createServerFn({ method: "GET" })
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
const supabase = await createServerClient();
const { data: word, error } = await supabase
.from("words")
.select("*")
.eq("id", data.id)
.single();
if (error) throw error;
return word;
});
From client components:
import { getWord } from "@/server/functions/get-word";
function WordComponent({ id }: { id: string }) {
const [word, setWord] = useState(null);
useEffect(() => {
getWord({ data: { id } }).then(setWord);
}, [id]);
return <div>{word?.latin}</div>;
}
From loaders:
export const Route = createFileRoute("/word/$id")({
loader: async ({ params }) => {
const word = await getWord({ data: { id: params.id } });
return { word };
},
});
Authenticated requests:
export const bookmarkWord = createServerFn({ method: "POST" })
.validator((data: { wordId: string }) => data)
.handler(async ({ data }) => {
const supabase = await createServerClient();
// Get authenticated user
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error || !user) {
throw new Error("Unauthorized");
}
// Create bookmark (RLS policy enforces ownership)
const { data: bookmark, error: bookmarkError } = await supabase
.from("bookmarks")
.insert({ user_id: user.id, word_id: data.wordId })
.select()
.single();
if (bookmarkError) throw bookmarkError;
return bookmark;
});
File uploads:
export const uploadImage = createServerFn({ method: "POST" })
.validator((data: { file: File }) => data)
.handler(async ({ data }) => {
const supabase = await createServerClient();
const { data: upload, error } = await supabase.storage
.from("images")
.upload(`${Date.now()}-${data.file.name}`, data.file);
if (error) throw error;
return upload;
});
Loaders fetch data before rendering:
export const Route = createFileRoute("/search")({
loader: async ({ context, search }) => {
const results = await searchWords(search.q);
return { results };
},
component: SearchPage,
});
function SearchPage() {
const { results } = Route.useLoaderData();
return <SearchResults results={results} />;
}
Load multiple resources in parallel: