On this page
File-based routing is useful when a directory communicates the same structure as the application. It becomes confusing when developers assume every filename creates a visible URL segment, or treat a loader as a private server endpoint. TanStack Router and TanStack Start share routing concepts, but they do not give every module the same execution environment.
Establish the routing and data contracts
TanStack Router supplies routing, typed navigation, route loaders, and search-parameter handling. TanStack Start builds a full-stack React framework around that router, adding server rendering and server-side capabilities. A Router application can be entirely client-side; choosing file-based routes alone does not turn it into a server application.
Before building routes, collect representative API payloads. Paste sanitized examples into JSON to TypeScript to draft types for records returned by loaders. Treat the output as a starting point: examples cannot prove that a field is required, nullable, or trustworthy. Add runtime validation at the boundary where untrusted responses enter your application.
Use the TanStack Router documentation for routing concepts and the TanStack Start documentation for framework setup. Install compatible versions through an official starter rather than combining configuration fragments from different release generations. Commit the lockfile and document your chosen runtime before implementing authentication.
Let filenames describe layout ownership
In a conventional file-routing setup, the root route lives in src/routes/__root.tsx. An index route represents a parent's exact URL, while a dynamic segment such as $projectId captures a parameter. Dotted filenames can express nesting without requiring matching physical folders for every level.
For example, projects.tsx can own the projects layout, projects.index.tsx its index, and projects.$projectId.tsx a detail route. A matched child renders through the parent's Outlet. If the parent forgets that outlet, navigation can update the URL while the expected detail content never appears.
A pathless layout groups routes under shared UI or checks without adding a URL segment. It is useful for authenticated application chrome, but should not become a hidden maze of unrelated concerns. Keep account settings, project navigation, and public marketing pages in clearly named branches.
The route-generation integration creates the route tree and its types. Do not hand-edit generated output or silence errors by casting route paths to arbitrary strings. If generation is missing, repair the configured plugin or generation command. Generated types are part of the navigation contract, not incidental editor decoration.
Validate URL state before using it
Path parameters identify resources; search parameters usually describe views of those resources. A project identifier belongs naturally in a path, while a page number or filter often belongs in the query string. Keeping shareable state in the URL supports refreshes, browser history, and links sent to coworkers.
This illustrative route validates a page number, includes it in loader dependencies, and passes the router's cancellation signal to fetch. It assumes a JSON endpoint that is safe to call from the browser and returns the expected project shape.
import { createFileRoute } from "@tanstack/react-router";
type Project = { id: string; name: string };
export const Route = createFileRoute("/projects/")({
validateSearch: (search: Record<string, unknown>) => {
const candidate = Number(search.page ?? 1);
return {
page: Number.isSafeInteger(candidate) && candidate > 0
? candidate
: 1,
};
},
loaderDeps: ({ search }) => ({ page: search.page }),
loader: async ({ deps, abortController }) => {
const response = await fetch(
`/api/projects?page=${deps.page}`,
{ signal: abortController.signal },
);
if (!response.ok) throw new Error("Projects unavailable");
return response.json() as Promise<Project[]>;
},
component: Projects,
});
function Projects() {
const projects = Route.useLoaderData();
return <ul>{projects.map(project => (
<li key={project.id}>{project.name}</li>
))}</ul>;
}The assertion in this compact example provides static typing, not runtime validation. Replace it with a schema parser when integrating a real service. Also enforce sensible upper bounds on pagination according to the API contract; a syntactically valid integer may still request unreasonable work.
Keep loader execution separate from secrets
A route loader is not inherently server-only. In a client-rendered Router application it executes in the browser, and in Start its execution participates in server rendering and client navigation. Never place database credentials or privileged SDK initialization into a route module on the assumption that its loader hides them.
In Start, use the documented server functions or server routes for operations that must execute on the server. Authenticate and authorize inside that trusted operation. A client-side redirect can improve navigation, but it is not an access-control boundary. Every protected read and write still needs a server-side decision.
Server rendering also changes assumptions about fetch. A relative URL that works in a browser may not resolve in a server runtime without request-aware handling. For Start, prefer an appropriate server function for direct backend access rather than blindly copying a browser fetch example into an SSR loader.
Keep request-specific sessions out of long-lived module globals. Otherwise one request's identity can leak into another request's work. The wider React frameworks category is useful for comparing rendering approaches, but execution location must be verified in the framework documentation for your actual version.
Coordinate loading, navigation, and recovery
Use loader dependencies to describe the validated search state that changes a route's data. Reading arbitrary query parameters elsewhere while leaving dependencies unchanged can produce confusing stale results. Choose a single owner for server-state caching if integrating a query library, rather than maintaining competing caches with unrelated freshness policies.
Configure pending, error, and not-found UI at meaningful route boundaries. A missing project is not the same as a network outage. Preserve the surrounding application shell and provide recovery actions appropriate to each failure. Review those states alongside the accessible interface guide, especially after client navigation changes the main content.
Use typed links and navigation APIs instead of concatenating URL strings. Parameters and search objects can then participate in compile-time checks. See the official navigation guide for the supported patterns.
Test direct entry, browser refresh, back and forward navigation, invalid search values, and links to removed records. Run the same scenarios after deployment, where fallback routing and server adapters can behave differently from a development server. A successful click from the home page does not prove a deep link works independently.
Key takeaways
- Separate Router's navigation model from Start's full-stack execution capabilities.
- Use generated route types and layouts that reflect ownership of visible interface regions.
- Validate URL state and declare the search values that drive loader results.
- Keep secrets and authorization in trusted server operations, not ordinary route modules.
- Test direct entry, loading failures, missing records, and browser history before shipping.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor