Skip to content

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Nov 21, 2025

Warning

UNTESTED, DO NOT MERGE

Use the params.parse option (https://tanstack.com/router/latest/docs/framework/react/api/router/RouteOptionsType#paramsparse-method) to validate path params while matching a route. This allows us to look for another match if the validation fails.

Pros: more flexible routing. Any $param can now become a regex route segment (or any validation you like).

Cons: routes with a params.parse method cannot share the same node as other routes of otherwise the same shape. If used frequently, this increases branching, which increases the number of branches we need to explore to find a match.


Some details:

  • routes w/ params.parse have priority over routes without (all else being equal)
  • extractParams is now "resumable" so we can call it at several point while matching without repeating work (at the expense of allocating more objects)

TODO:

  • find a way to store and return the parsed params from params.parse to avoid duplicate work after matching
  • when several params.parse function are on the way, should the 2nd receive the output of the 1st? Or do they both receive the raw Record<string,string>?
  • error during params.parse should be preserved to be set in the store or thrown if opts?.throwOnError
  • why is there routeParams and strictParams?
  • support deprecated options.parseParams in addition to options.params.parse
  • should notFound and redirect errors still be respected when we're trying to "continue matching" despite the parsing error?

I think each parse function receives the output of the previous one, see below. But I also think this means we can't decode it to something not stringifyable by String() (i.e. objects) because this partially parsed object will be used in interpolatePath to generate part of the matchId

if (!existingMatch) {
const strictParseParams =
route.options.params?.parse ?? route.options.parseParams
if (strictParseParams) {
try {
Object.assign(
strictParams,
strictParseParams(strictParams as Record<string, string>),
)
} catch (err: any) {
if (isNotFound(err) || isRedirect(err)) {
paramsError = err
} else {
paramsError = new PathParamError(err.message, {
cause: err,
})
}
if (opts?.throwOnError) {
throw paramsError
}
}
}
}

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

Walkthrough

This change extends the routing engine to support route-level custom parameter parsing. Route options now accept a parse function that is stored on segment nodes during tree construction. During route matching, parameters are extracted and parsed early in the traversal; parse errors prune the current path from consideration.

Changes

Cohort / File(s) Summary
Route-level parameter parsing support
packages/router-core/src/new-process-route-tree.ts
Added parse field to SegmentNode<T> type and RouteLike.options.params to store and apply custom parameter parsing functions. Modified extractParams to return [params, state] tuple. Updated getNodeMatch to apply node.parse when present. Enhanced MatchStackFrame with extract and params fields to thread parsing state through the stack. Adjusted sortDynamic to prioritize nodes with parse functions. Updated all node creation functions (createStaticNode, createDynamicNode, etc.) to initialize parse: null. Modified matching paths (static, dynamic, optional, wildcard) to propagate parse state consistently.

Sequence Diagram

sequenceDiagram
    participant Router as Route Matching Engine
    participant Tree as Segment Tree
    participant Node as Segment Node
    participant Parse as Parse Function
    participant Result as Match Result

    Router->>Tree: Start traversal
    Router->>Node: Check for parse function
    alt parse function exists
        Node-->>Router: parse function found
        Router->>Router: Extract params early
        Router->>Parse: Apply parse(params)
        alt parse succeeds
            Parse-->>Router: Parsed params
            Router->>Router: Attach to frame
            Router->>Result: Continue matching with parsed params
        else parse throws error
            Parse-->>Router: Error
            Router->>Router: Prune current path
            Router->>Result: Try alternate routes
        end
    else no parse function
        Node-->>Router: parse = null
        Router->>Result: Continue standard matching
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Core matching logic refactoring: Dense changes to the route matching pipeline affecting parameter extraction, frame propagation, and error handling
  • Type signature changes: Multiple updates to SegmentNode, MatchStackFrame, and function signatures require careful verification
  • Behavioral changes in error handling: Parse failures now prune paths—verify this integrates correctly with existing match fallback logic
  • Sorting logic update: sortDynamic now considers parse presence; review prioritization semantics to ensure correct node ordering

Possibly related PRs

Suggested labels

package: router-core

Suggested reviewers

  • schiller-manuel

Poem

🐰 A rabbit hops through routes with grace,
Parse functions now find their place,
Parameters bend to custom will,
Errors prune with graceful skill,
The tree grows wise, the matches thrill! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(router-core): validate params while matching [WIP]' accurately describes the main change: introducing parameter validation during route matching through route-level param parsing hooks.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-router-core-validate-params-while-matching

Comment @coderabbitai help to get the list of available commands and usage tips.

@Sheraff Sheraff changed the title feat(router-core): validate params while matching feat(router-core): validate params while matching [WIP] Nov 21, 2025
@nx-cloud
Copy link

nx-cloud bot commented Nov 21, 2025

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit d9e403b

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 11m 37s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 21s View ↗

☁️ Nx Cloud last updated this comment at 2025-11-21 20:58:22 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 21, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5936

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5936

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5936

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5936

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@5936

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5936

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5936

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5936

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5936

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5936

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5936

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5936

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5936

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5936

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5936

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5936

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5936

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5936

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5936

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5936

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5936

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5936

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5936

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-ssr-query@5936

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5936

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5936

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5936

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5936

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5936

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5936

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@5936

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5936

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5936

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5936

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5936

commit: d9e403b

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/router-core/src/new-process-route-tree.ts (1)

917-919: Silent error handling may hinder debugging.

Parse errors are caught and silently cause the route to be skipped via continue. While this enables fallback to alternative routes, it provides no visibility into why a route was rejected, making debugging difficult for developers.

Consider adding optional logging or a debug mode that reports parse failures, especially during development.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc79c97 and d9e403b.

📒 Files selected for processing (1)
  • packages/router-core/src/new-process-route-tree.ts (21 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/router-core/src/new-process-route-tree.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (4)
packages/router-core/src/new-process-route-tree.ts (4)

375-376: Verify parse prioritization is always desired.

The sort function unconditionally prioritizes nodes with parse functions over those without. While this makes sense for validation (try more specific routes first), it's hardcoded with no way to customize priority.

Consider whether this ordering is always appropriate, especially when multiple routes with different parse functions compete. The current tie-breaking logic (lines 377-394) would then determine order, which might be arbitrary.


236-244: Node reuse prevented when parse is present.

Routes with parse functions cannot share segment nodes, even if they're otherwise identical. This conservative approach is safe (prevents incorrect sharing of different parse functions), but increases memory usage if many routes have parse functions.

This is likely acceptable for correctness, but worth noting for performance-sensitive applications with large route trees.

Also applies to: 278-286


765-767: LGTM: Param extraction correctly resumes from saved state.

The logic properly handles incremental param extraction:

  1. Resumes from leaf.extract state if present (line 765-767)
  2. Extracts params from remaining nodes
  3. Merges previously extracted params back in (line 823)

This avoids re-extracting and re-validating params when multiple parse functions exist in the route path.

Also applies to: 823-824


534-547: LGTM: Type definitions are consistent with implementation.

The parse field is properly typed across SegmentNode, RouteLike, and MatchStackFrame. The function signature (params: Record<string, string>) => any accurately reflects that params are strings and the return type is currently unused.

Comment on lines +908 to +920
if (node.parse) {
// if there is a parse function, we need to extract the params that we have so far and run it.
// if this function throws, we cannot consider this a valid match
try {
;[params, extract] = extractParams(path, parts, frame)
// TODO: can we store the parsed value somewhere to avoid re-parsing later?
node.parse(params)
frame.extract = extract
frame.params = params
} catch {
continue
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Parse function return value is ignored.

The parse function is typed to return any (line 546), but the return value is discarded on line 914. This creates ambiguity about whether parse should validate (throw on error) or transform (return modified params). The current implementation only supports validation.

Consider either:

  • Documenting that parse is validation-only and updating the return type to void
  • Or storing and using the return value to support param transformation

The TODO on line 913 hints at storing results, which would enable transformation. If transformation isn't needed, clarify this in the type signature.


🌐 Web query:

Does TanStack Router support parameter transformation or only validation in parse functions?

💡 Result:

Short answer: parse functions can transform (coerce/convert) parameters — not just validate them. The router calls your params.parse with the raw string values and uses whatever typed/transformed values you return (stringify is the reverse). That said, there are a few reported type-inference/coercion issues in some versions/adapters (see the Zod issue). [1][2]

Sources:

  • TanStack Router docs — custom parameter parsing (params.parse / params.stringify) and search‑param parsing/validation. [1]
  • GitHub issue reporting type/coercion/inference problems with validators/adapters (example with Zod). [2]

Parse function return value must be captured and used for parameter transformation.

The web search confirms that TanStack Router parse functions can transform (coerce/convert) parameters, and the router uses whatever typed/transformed values the parse function returns. However, the PR code on line 914 calls node.parse(params) but discards the return value, preventing parameter transformation.

Fix required at line 908-920: Capture the parse return value and use it to update params:

try {
  ;[params, extract] = extractParams(path, parts, frame)
  params = node.parse(params) ?? params  // Capture and use transformed value
  frame.extract = extract
  frame.params = params
} catch {
  continue
}

Without this fix, routes with parse functions cannot perform parameter type coercion/conversion.

🤖 Prompt for AI Agents
In packages/router-core/src/new-process-route-tree.ts around lines 908 to 920,
the parse function's return value is ignored so transformed/coerced params are
not applied; update the try block to capture node.parse's return and assign it
back to params (using a nullish fallback to the original params if parse returns
undefined), then set frame.extract and frame.params to the updated values; keep
the existing error handling (catch/continue) intact.

@birkskyum birkskyum marked this pull request as draft November 21, 2025 23:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants