@tanstack/router-core 1.169.0 → 1.169.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,10 @@ sources:
21
21
 
22
22
  # Auth and Guards
23
23
 
24
+ > **This skill covers the routing side of auth.** For the **server-side primitives** — session cookies (`HttpOnly`/`Secure`/`SameSite`), `useSession`-style helpers, OAuth `state` + PKCE, password-reset enumeration defense, CSRF, rate limiting — see [start-core/auth-server-primitives](../../../../start-client-core/skills/start-core/auth-server-primitives/SKILL.md). The two skills are designed to be used together.
25
+ >
26
+ > **CRITICAL**: A route guard (`beforeLoad`) does NOT protect a `createServerFn` declared on that route. Server functions are RPC endpoints reachable by direct POST regardless of which route renders them. See "Route guards do not protect server functions" below.
27
+
24
28
  ## Setup
25
29
 
26
30
  Protect routes with `beforeLoad` + `redirect()` in a pathless layout route (`_authenticated`):
@@ -371,6 +375,41 @@ export const Route = createFileRoute('/_authenticated')({
371
375
 
372
376
  ## Common Mistakes
373
377
 
378
+ ### CRITICAL: Route guards do not protect server functions
379
+
380
+ A `beforeLoad` redirect protects the **route's UI**, not the **server functions** declared on it. `createServerFn` produces an RPC endpoint reachable by direct POST regardless of which route renders the calling UI. An attacker doesn't have to load `/_authenticated/orders` — they can curl the RPC endpoint directly.
381
+
382
+ ```tsx
383
+ // WRONG — handler has no auth check; the route guard doesn't help
384
+ import { createServerFn } from '@tanstack/react-start'
385
+ import { createFileRoute, redirect } from '@tanstack/react-router'
386
+
387
+ const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
388
+ return db.orders.findMany() // ← anyone can hit the RPC
389
+ })
390
+
391
+ export const Route = createFileRoute('/_authenticated/orders')({
392
+ beforeLoad: ({ context }) => {
393
+ if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
394
+ },
395
+ loader: () => getMyOrders(),
396
+ })
397
+ ```
398
+
399
+ ```tsx
400
+ // CORRECT — auth enforced on the handler itself, via middleware
401
+ import { createServerFn } from '@tanstack/react-start'
402
+ import { authMiddleware } from '~/server/auth-middleware'
403
+
404
+ const getMyOrders = createServerFn({ method: 'GET' })
405
+ .middleware([authMiddleware])
406
+ .handler(async ({ context }) => {
407
+ return db.orders.findMany({ where: { userId: context.session.userId } })
408
+ })
409
+ ```
410
+
411
+ Rule of thumb: every `createServerFn` that touches user data needs `authMiddleware` (or an equivalent in-handler check). The route guard is for the page experience; the RPC guard is for the data. See [start-core/auth-server-primitives](../../../../start-client-core/skills/start-core/auth-server-primitives/SKILL.md) for the full session/middleware pattern.
412
+
374
413
  ### HIGH: Auth check in component instead of beforeLoad
375
414
 
376
415
  Component-level auth checks cause a **flash of protected content** before the redirect:
@@ -456,3 +495,5 @@ Place protected routes as children of the `_authenticated` layout route. Public
456
495
  ## Cross-References
457
496
 
458
497
  - See also: **router-core/data-loading/SKILL.md** — `beforeLoad` runs before `loader`; auth context flows into loader via route context
498
+ - See also: **start-core/auth-server-primitives/SKILL.md** — server-side session cookies, OAuth state + PKCE, CSRF, password-reset hardening, rate limiting (the server half of authentication)
499
+ - See also: **start-core/middleware/SKILL.md** — `authMiddleware` factory pattern for protecting individual `createServerFn` calls
@@ -391,22 +391,74 @@ component: () => {
391
391
  }
392
392
  ```
393
393
 
394
- ### 3. CRITICAL: Generating Next.js or Remix SSR patterns
394
+ ### 3. CRITICAL: Generating Next.js, Remix, or React Router DOM patterns
395
395
 
396
- TanStack Router does NOT use `getServerSideProps`, `getStaticProps`, App Router `page.tsx`, or Remix-style server-only `loader` exports.
396
+ TanStack Router does NOT use `getServerSideProps`, `getStaticProps`, App Router `page.tsx`, Remix-style server-only `loader` exports, or anything from `react-router-dom`.
397
+
398
+ #### Wrong file structures
399
+
400
+ ```text
401
+ WRONG (Next.js Pages Router):
402
+ src/pages/index.tsx
403
+ src/pages/_app.tsx
404
+ src/pages/posts/[id].tsx
405
+
406
+ WRONG (Next.js App Router):
407
+ app/layout.tsx
408
+ app/page.tsx
409
+ app/posts/[id]/page.tsx
410
+
411
+ WRONG (Next.js custom App):
412
+ _app/index.tsx
413
+ pages/_app.tsx, pages/_document.tsx
414
+
415
+ CORRECT (TanStack Router file-based routing):
416
+ src/routes/__root.tsx
417
+ src/routes/index.tsx
418
+ src/routes/posts/$postId.tsx
419
+ ```
420
+
421
+ #### Wrong imports
397
422
 
398
423
  ```tsx
399
- // WRONG — Next.js patterns
424
+ // WRONG — react-router-dom is a different library
425
+ import {
426
+ Link,
427
+ useNavigate,
428
+ BrowserRouter,
429
+ Route,
430
+ Routes,
431
+ } from 'react-router-dom'
432
+
433
+ // WRONG — Next.js Link/router
434
+ import Link from 'next/link'
435
+ import { useRouter } from 'next/router' // Pages Router
436
+ import { useRouter } from 'next/navigation' // App Router
437
+
438
+ // CORRECT — everything routing-related lives in @tanstack/react-router
439
+ import {
440
+ Link,
441
+ useNavigate,
442
+ useRouter,
443
+ useLocation,
444
+ redirect,
445
+ } from '@tanstack/react-router'
446
+ ```
447
+
448
+ #### Wrong loader/data-fetching patterns
449
+
450
+ ```tsx
451
+ // WRONG — Next.js Pages Router
400
452
  export async function getServerSideProps() {
401
453
  return { props: { data: await fetchData() } }
402
454
  }
403
455
 
404
- // WRONG — Remix patterns
456
+ // WRONG — Remix
405
457
  export async function loader({ request }: LoaderFunctionArgs) {
406
458
  return json({ data: await fetchData() })
407
459
  }
408
460
 
409
- // CORRECT — TanStack Router pattern
461
+ // CORRECT — TanStack Router
410
462
  export const Route = createFileRoute('/data')({
411
463
  loader: async () => {
412
464
  const data = await fetchData()
@@ -421,6 +473,8 @@ function DataPage() {
421
473
  }
422
474
  ```
423
475
 
476
+ If you see `src/pages/`, `app/layout.tsx`, `react-router-dom`, or any of the above in agent output, the agent is generating for the wrong framework. The build will either fail or produce duplicate `/` routes that conflict at runtime.
477
+
424
478
  ## Tension: Client-First Loaders vs SSR
425
479
 
426
480
  TanStack Router loaders are client-first by design. When SSR is enabled, they run in both environments. This means:
@@ -313,7 +313,9 @@ export function NavItem(props: NavItemProps): React.ReactNode {
313
313
  <NavItem label="Post" linkOptions={{ to: '/posts/$postId', params: { postId: '1' } }} />
314
314
  ```
315
315
 
316
- ### `ValidateNavigateOptions` Type-Safe Navigate in Utilities
316
+ ### `ValidateNavigateOptions` and `ValidateRedirectOptions`
317
+
318
+ Same pattern as `ValidateLinkOptions` above, for `useNavigate` and `redirect`. Declare a generic public overload plus a non-generic implementation signature so the call site stays narrowed and the body works without casts:
317
319
 
318
320
  ```tsx
319
321
  import {
@@ -332,45 +334,15 @@ export function useDelayedNavigate<
332
334
  export function useDelayedNavigate(
333
335
  options: ValidateNavigateOptions,
334
336
  delayMs: number,
335
- ): () => void {
337
+ ) {
336
338
  const navigate = useNavigate()
337
339
  return () => {
338
340
  setTimeout(() => navigate(options), delayMs)
339
341
  }
340
342
  }
341
-
342
- // Usage — type-safe
343
- const go = useDelayedNavigate(
344
- { to: '/posts/$postId', params: { postId: '1' } },
345
- 500,
346
- )
347
343
  ```
348
344
 
349
- ### `ValidateRedirectOptions` — Type-Safe Redirect in Utilities
350
-
351
- ```tsx
352
- import {
353
- redirect,
354
- type RegisteredRouter,
355
- type ValidateRedirectOptions,
356
- } from '@tanstack/react-router'
357
-
358
- export async function fetchOrRedirect<
359
- TRouter extends RegisteredRouter = RegisteredRouter,
360
- TOptions = unknown,
361
- >(
362
- url: string,
363
- redirectOptions: ValidateRedirectOptions<TRouter, TOptions>,
364
- ): Promise<unknown>
365
- export async function fetchOrRedirect(
366
- url: string,
367
- redirectOptions: ValidateRedirectOptions,
368
- ): Promise<unknown> {
369
- const response = await fetch(url)
370
- if (!response.ok && response.status === 401) throw redirect(redirectOptions)
371
- return response.json()
372
- }
373
- ```
345
+ `ValidateRedirectOptions` works identically declare a generic overload accepting `ValidateRedirectOptions<TRouter, TOptions>` and an impl signature accepting `ValidateRedirectOptions`, then call `redirect(options)` in the body.
374
346
 
375
347
  ### Render Props for Maximum Performance
376
348
 
@@ -477,21 +449,44 @@ declare module '@tanstack/react-router' {
477
449
  }
478
450
  ```
479
451
 
480
- ### 5. CRITICAL (cross-skill): Generating Next.js or Remix patterns
452
+ ### 5. CRITICAL (cross-skill): Wrong-framework imports and file structure
453
+
454
+ Wrong-framework code looks plausible (it's React) but breaks the build or produces conflicting `/` routes at runtime.
481
455
 
482
456
  ```tsx
483
- // WRONG — these are NOT TanStack Router APIs
484
- export async function getServerSideProps() { ... }
485
- export async function loader({ request }) { ... } // Remix-style
486
- const [searchParams, setSearchParams] = useSearchParams() // React Router
457
+ // WRONG — react-router-dom and next/* are different libraries
458
+ import { Link, useNavigate, useSearchParams } from 'react-router-dom'
459
+ import Link from 'next/link'
460
+ import { useRouter, useParams } from 'next/navigation'
487
461
 
488
- // CORRECT — TanStack Router APIs
462
+ // CORRECT — all routing exports come from @tanstack/react-router
463
+ import {
464
+ Link,
465
+ Outlet,
466
+ useNavigate,
467
+ useRouter,
468
+ useLocation,
469
+ useParams,
470
+ redirect,
471
+ } from '@tanstack/react-router'
472
+ ```
473
+
474
+ ```tsx
475
+ // WRONG file structures + APIs:
476
+ // src/pages/*.tsx with getServerSideProps / getStaticProps (Next.js Pages Router)
477
+ // app/layout.tsx + app/page.tsx (Next.js App Router)
478
+ // _app/index.tsx, pages/_app.tsx, pages/_document.tsx (Next.js custom App)
479
+ // loader/action exports (Remix)
480
+
481
+ // CORRECT — TanStack file-based routing at src/routes/*.tsx
489
482
  export const Route = createFileRoute('/posts')({
490
- loader: async () => { ... }, // TanStack loader
491
- validateSearch: zodValidator(schema), // TanStack search validation
483
+ loader: async () => { ... },
484
+ validateSearch: zodValidator(schema),
492
485
  component: PostsComponent,
493
486
  })
494
- const search = Route.useSearch() // TanStack hook
487
+ const search = Route.useSearch()
495
488
  ```
496
489
 
490
+ If a build error mentions `react-router-dom`, `next/`, `pages/_app`, or duplicate `/` routes, fix the import — don't paper over with type assertions.
491
+
497
492
  See also: router-core (Register setup), router-core/navigation (from narrowing), router-core/code-splitting (getRouteApi).
@@ -713,7 +713,7 @@ const runLoader = async (
713
713
  const pendingPromise = match._nonReactive.minPendingPromise
714
714
  if (pendingPromise) await pendingPromise
715
715
 
716
- // Last but not least, wait for the the components
716
+ // Last but not least, wait for the components
717
717
  // to be preloaded before we resolve the match
718
718
  if (route._componentsPromise) await route._componentsPromise
719
719
  inner.updateMatch(matchId, (prev) => ({
package/src/route.ts CHANGED
@@ -173,11 +173,13 @@ export type ResolveParams<
173
173
 
174
174
  export type ParseParamsFn<in out TPath extends string, in out TParams> = (
175
175
  rawParams: Expand<ResolveParams<TPath>>,
176
- ) =>
177
- | (TParams extends ResolveParams<TPath, any>
178
- ? TParams
179
- : ResolveParams<TPath, any>)
180
- | false
176
+ ) => TParams | false
177
+
178
+ type ValidateParsedParams<TPath extends string, TParams> = [TParams] extends [
179
+ ResolveParams<TPath, any>,
180
+ ]
181
+ ? unknown
182
+ : never
181
183
 
182
184
  export type StringifyParamsFn<in out TPath extends string, in out TParams> = (
183
185
  params: TParams,
@@ -185,14 +187,15 @@ export type StringifyParamsFn<in out TPath extends string, in out TParams> = (
185
187
 
186
188
  export type ParamsOptions<in out TPath extends string, in out TParams> = {
187
189
  params?: {
188
- parse?: ParseParamsFn<TPath, TParams>
190
+ parse?: ParseParamsFn<TPath, TParams> & ValidateParsedParams<TPath, TParams>
189
191
  stringify?: StringifyParamsFn<TPath, TParams>
190
192
  }
191
193
 
192
194
  /**
193
195
  @deprecated Use params.parse instead
194
196
  */
195
- parseParams?: ParseParamsFn<TPath, TParams>
197
+ parseParams?: ParseParamsFn<TPath, TParams> &
198
+ ValidateParsedParams<TPath, TParams>
196
199
 
197
200
  /**
198
201
  @deprecated Use params.stringify instead
package/bin/intent.js DELETED
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env node
2
- // Auto-generated by @tanstack/intent setup
3
- // Exposes the intent end-user CLI for consumers of this library.
4
- // Commit this file, then add to your package.json:
5
- // "bin": { "intent": "./bin/intent.js" }
6
- try {
7
- await import('@tanstack/intent/intent-library')
8
- } catch (e) {
9
- const isModuleNotFound =
10
- e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND'
11
- const missingIntentLibrary =
12
- typeof e?.message === 'string' && e.message.includes('@tanstack/intent')
13
-
14
- if (isModuleNotFound && missingIntentLibrary) {
15
- console.error('@tanstack/intent is not installed.')
16
- console.error('')
17
- console.error('Install it as a dev dependency:')
18
- console.error(' npm add -D @tanstack/intent')
19
- console.error('')
20
- console.error('Or run directly:')
21
- console.error(' npx @tanstack/intent@latest list')
22
- process.exit(1)
23
- }
24
- throw e
25
- }