web-routing-tanstack-router-fe1fc0 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-routing-tanstack-router-fe1fc0 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: TanStack Router types the whole URL — path params, search params and loader return values all flow through inference, so a wrong<Link to>is a compile error. The router plugin generatesrouteTree.gen.tsfrom the routes directory;validateSearchturns search params into a validated schema;beforeLoadruns sequentially for guards and context,loaderruns in parallel for data. Registering the router throughdeclare moduleis what makes the type safety app-wide.
Detailed Resources:
Link, active options, search updaters, useNavigate, redirectbeforeLoad, prefetch, blocking vs non-blocking, SWR cachinggetRouteApiLink props, router options, file-naming tablecreateFileRoute("/path"), the path string is checked against the file's location, and autoCodeSplitting splits each route for free. Everything in examples/routes.md assumes this.createRoute({ getParentRoute, path }). Every route option below still applies, but the file-naming conventions do not, and the tree is yours to keep in sync.Mixing the two in one project puts a hand-written route outside the generated tree, where the plugin cannot see it.
<critical_requirements>
Declare routes with `createFileRoute` while the plugin is generating the tree. The path string is then validated against the file's real location, and the generated tree stays the single source of truth.
Read search params through `validateSearch` and `useSearch()`. They arrive typed, with defaults applied and invalid values already handled, which reading window.location.search gives up.
Put auth checks in `beforeLoad`. It runs before any component mounts and before child loaders fire, so a throw redirect() there costs no render and no wasted fetch.
Hand services to routes through router context rather than importing them into loaders. A loader that reads context.apiClient can be tested against a substituted client.
Render `<Outlet />` in every layout route. It is the slot the matched child renders into, and a layout without one renders its children nowhere, silently.
Register the router with `declare module "@tanstack/react-router"`. That single declaration is what gives Link, useNavigate and redirect their route-aware types across the app.
</critical_requirements>
Auto-detection: createFileRoute, createRootRouteWithContext, createLazyFileRoute, routeTree.gen, @tanstack/react-router, @tanstack/router-plugin, validateSearch, zodValidator, beforeLoad, loaderDeps, useRouteContext, getRouteApi, notFound, retainSearchParams, stripSearchParams, activeProps, preload="intent"
Applies to:
Handled elsewhere:
validateSearch accepts any Standard Schema validator or a plain function, and how the schema is written belongs to whatever owns validation.staleTime; a cache that serves many components from one entry is a separate concern the router injects through context.data-status="active" and aria-current="page", and what is done with them is not the router's business.<philosophy>
TanStack Router treats the URL as typed state. Search params are validated schemas rather than strings, loader return types flow into useLoaderData(), and destinations are checked against the generated tree — so routing bugs surface at compile time instead of on a broken link.
routeTree.gen.ts; nothing is registered by hand.createRootRouteWithContext, and each beforeLoad can add to what its children see.beforeLoad is the sequential phase; putting fetches there is what creates a waterfall.</philosophy>
<decision_framework>
What does this code do?
+-- Auth check or permission guard? -> beforeLoad (runs first, blocks everything below)
+-- Conditional redirect? -> beforeLoad (throw redirect())
+-- Add data children will need? -> beforeLoad (its return value merges into context)
+-- Fetch data for this component? -> loader (parallel with sibling loaders)Where are you?
+-- In JSX, on something clickable? -> <Link to="..." params={...} />
+-- In a handler, after a mutation? -> useNavigate()
+-- In beforeLoad or a loader? -> throw redirect()
+-- Outside the React tree entirely? -> router.navigate() on the router instanceThe last one is the only route out of a module that never renders — a fetch wrapper redirecting on a 401, for instance. The hooks all need a component.
What is shared, and does it belong in the URL?
+-- Shared UI under a URL segment? -> route.tsx in that directory
+-- Shared UI or a guard, no segment? -> pathless layout (_authenticated.tsx)
+-- URL nests but layout should not? -> non-nested suffix (posts_.create.tsx)
+-- Grouping for the file tree only? -> group directory ((admin)/)Reach for the Zod adapter once a schema has several fields or is shared with a form, and for a plain validateSearch function for one or two params where a dependency buys nothing. Zod 3.24+ and Zod 4 implement Standard Schema and can be passed directly, using .catch() where the adapter's fallback() would have gone.
staleTime on a route is enough while the data belongs to that route. Once several components across the app need the same server data, inject a cache client through context and prefetch in the loader — and drop staleTime to 0 there, since two caches both deciding freshness fetch twice.
</decision_framework>
<patterns>
The plugin runs before the React plugin and generates the tree; declare module registers the router so every Link and navigate is typed.
// bundler config — router plugin first
tanstackRouter({ target: "react", autoCodeSplitting: true }),
react(),const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}Full code: examples/core.md
A file's location determines its URL, and its createFileRoute path string has to match. The prefixes and suffixes that change nesting — _pathless, posts_.escaped, $param, $ splat, (group), -ignored — are tabulated in reference.md.
// src/routes/posts/index.tsx -> /posts
export const Route = createFileRoute("/posts/")({
component: PostsIndex,
});Full code: examples/routes.md
Destinations, params and search are all checked against the generated tree. preload="intent" warms the route on hover.
<Link to="/posts/$postId" params={{ postId: post.id }} preload="intent">
{post.title}
</Link>;
const navigate = useNavigate();
await navigate({ to: "/posts/$postId", params: { postId: post.id }, replace: true });
throw redirect({ to: "/login", search: { redirect: location.href } });Full code: examples/navigation.md
validateSearch turns the query string into a typed object. fallback() keeps an invalid param from throwing, and .default() fills a missing one.
const schema = z.object({
page: fallback(z.number().min(1), DEFAULT_PAGE).default(DEFAULT_PAGE),
q: fallback(z.string(), "").default(""),
});
export const Route = createFileRoute("/products/")({
validateSearch: zodValidator(schema),
component: ProductsPage,
});
const { page, q } = Route.useSearch();Full code: examples/search-params.md
Loaders run before render, in parallel with siblings, and receive an abortController whose signal cancels the fetch when the user navigates away.
export const Route = createFileRoute("/posts/$postId/")({
staleTime: STALE_TIME_MS,
loader: async ({ params, context, abortController }) => {
const post = await context.apiClient.getPost(params.postId, {
signal: abortController.signal,
});
return { post };
},
component: PostDetail,
});Full code: examples/data-loading.md
A layout route wraps its children with shared UI. A _ prefix makes the layout pathless — it adds UI and guards without adding a URL segment.
// src/routes/_authenticated.tsx -> children live at /dashboard, /settings
export const Route = createFileRoute("/_authenticated")({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: "/login" });
},
component: () => <Outlet />,
});Full code: examples/routes.md
createRootRouteWithContext fixes the context shape; createRouter supplies it; every loader and beforeLoad receives it typed. A beforeLoad return value merges into what its children see.
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
});
const router = createRouter({ routeTree, context: { auth, apiClient } });
loader: async ({ context }) => ({ posts: await context.apiClient.getPosts() });Full code: examples/auth-and-context.md
Each is a route option, and createRouter takes a default* version of each for routes that declare none. pendingMs delays the spinner past a fast load; pendingMinMs stops it flickering.
export const Route = createFileRoute("/posts/$postId/")({
pendingMs: PENDING_DELAY_MS,
pendingComponent: () => <div>Loading…</div>,
errorComponent: ({ error, reset }) => (
<div role="alert">
<pre>{error.message}</pre>
<button type="button" onClick={reset}>Retry</button>
</div>
),
notFoundComponent: () => <div>Post not found</div>,
loader: async ({ params }) => {
const post = await fetchPost(params.postId);
if (!post) throw notFound();
return { post };
},
component: PostDetail,
});Full code: examples/error-handling.md
</patterns>
<red_flags>
Breaks at runtime:
return redirect() or return notFound() — both have to be thrown to short-circuit the match.<Outlet /> — children render nowhere, with no error.routeTree.gen.ts — it is regenerated on the next run and the edit is gone.createFileRoute path string that does not match the file's location.createRoute by hand while the plugin is running — the route sits outside the generated tree.beforeLoad — the protected view paints before the redirect, and child loaders have already run unauthenticated.window.location.search in place of useSearch() — no validation, no defaults, and no re-render when the URL changes.Surprising behaviour:
beforeLoad is the sequential phase, parent before child; a fetch there serialises what loader would have run in parallel.fallback(), an invalid search param throws instead of falling back to the default.declare module registration costs type safety silently — Link and navigate still compile, just untyped.useSearch({ strict: false }) returns a partial type, and is only right when the current route genuinely is unknown.staleTime: 0 alongside an external cache double-fetches on every navigation.to="." without from resolves against whichever route the component happens to be under.</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.