Skip to content

Conversation

@dannote
Copy link

@dannote dannote commented Jan 12, 2026

Summary

This switches typed route generation + matching to a nested route tree, so $fetch types walk path segments instead of scoring every route key. It keeps method narrowing intact while avoiding the deep type instantiation that slows or breaks TypeScript on larger APIs.

What changed

  • Generate a nested InternalApi with a $ method bucket at each leaf (src/build/types.ts).
  • Replace score-based matching with a segment-walk matcher that normalizes paths and resolves exact -> param -> catch-all (src/types/fetch/_match.ts).
  • Recompute NitroFetchRequest as a union of nested route paths and use matched method maps for $fetch narrowing (src/types/fetch/fetch.ts).
  • Add type-level fixtures for exact, param, and catch-all cases (test/fixture/typed-routes.types.ts).

Why this helps

The previous flat route map required comparing every route on each $fetch call (O(n x path depth)). The nested tree resolves in O(path depth) and dramatically reduces TypeScript instantiations.

Related issues

Elysia/Eden inspiration

The nested route tree idea is inspired by Elysia's CreateEden and Eden's path mapping utilities:

Tests

  • pnpm lint:fix
  • pnpm test:types
  • pnpm test (fails in test:rollup with Vitest unhandled ECONNRESET / "Server is not running"; test:rolldown did not run)

@dannote dannote requested a review from pi0 as a code owner January 12, 2026 21:27
@vercel
Copy link

vercel bot commented Jan 12, 2026

@dannote is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Jan 12, 2026

📝 Walkthrough

Walkthrough

Restructures the route typing system from a flat, static approach to a hierarchical tree-based model. Introduces path normalization, exact and loose matching strategies, and nested route schemas to support better type safety and improved code organization. Changes span type generation, route resolution logic, and test infrastructure.

Changes

Cohort / File(s) Summary
Route type generation
src/build/types.ts
Replaces static route-typing generator with tree-based builder. Introduces buildRouteTree and renderRouteTree functions to construct and recursively render nested route hierarchies, replacing direct string assembly. Generates nested InternalApi structure with $-based HTTP method maps.
Route matching and path resolution
src/types/fetch/_match.ts, src/types/fetch/fetch.ts
Replaces complex match-scoring system with modular route matching schema. Adds public path normalization types (NormalizePath, StripQuery, StripHash, StripLeadingSlash, StripTrailingSlash, SplitPath, JoinPath, RootPath), exact/loose matching flow (ExactMatch, LooseMatch), and simplified MatchedRoutes signature. Updates internal route lookup and method resolution logic.
Type test fixtures
test/fixture/typed-routes.types.ts
New test fixture for TypeScript compile-time type verification. Declares nested API schema under InternalApi.api with users and files routes, including parameters and HTTP methods. Provides type-level assertions validating route typing against TypedInternalResponse and AvailableRouterMethod.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(types): speed up typed routes' follows conventional commits format with type, scope, and descriptive subject, and accurately reflects the main change of optimizing typed route performance through nested route tree implementation.
Description check ✅ Passed The PR description is directly related to the changeset, providing a clear summary of changes, rationale for the performance improvement, related issues, and test results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@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: 2

🤖 Fix all issues with AI agents
In @src/build/types.ts:
- Around line 146-152: The generated method entries for node.$ are missing
trailing semicolons; update the code that pushes method lines (the loop using
lines.push(`${childIndent}'${method}': ${types.join(" | ")}`)) to append a
semicolon so each entry becomes `${childIndent}'${method}': ${types.join(" |
")};`, ensuring valid TypeScript in the emitted declarations. Ensure the change
is applied where indent/childIndent and node.$ are handled so all rendered
method lines include the semicolon.
- Around line 171-185: The generateRoutes function currently injects a local
Awaited type declaration that shadows TypeScript's built-in Awaited; remove the
redundant "type Awaited..." line from the generated output in generateRoutes so
the module augmentation relies on the built-in utility instead—update
generateRoutes (which builds routeTree via buildRouteTree and renders with
renderRouteTree) to omit the manual Awaited declaration while keeping the rest
of the declared module and exported empty module.
🧹 Nitpick comments (2)
test/fixture/typed-routes.types.ts (1)

39-53: Consider adding a test for default method handling.

The test coverage is solid for explicit HTTP methods, but the AvailableRouterMethod logic in fetch.ts has special handling for the "default" method case. Consider adding a test case with a route that has a default method to verify the RouterMethod fallback behavior.

src/types/fetch/fetch.ts (1)

11-29: Duplicate JoinPath and RootPath type utilities.

JoinPath (lines 13-15) and RootPath (line 17) are duplicated in src/types/fetch/_match.ts (lines 38-42). While this may be intentional to avoid circular dependencies (since _match.ts imports from fetch.ts), consider extracting these shared utilities to a common module like _utils.ts to improve maintainability.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99691fc and 8449e52.

📒 Files selected for processing (4)
  • src/build/types.ts
  • src/types/fetch/_match.ts
  • src/types/fetch/fetch.ts
  • test/fixture/typed-routes.types.ts
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.{ts,js,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,js,tsx,jsx}: Use pathe for cross-platform path operations instead of Node.js node:path
Use ESM and modern JavaScript
Do not add comments explaining what the line does unless prompted

Files:

  • src/build/types.ts
  • src/types/fetch/fetch.ts
  • src/types/fetch/_match.ts
src/{build,dev,runner,cli}/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use consola for logging in build/dev code; use nitro.logger when available

Files:

  • src/build/types.ts
src/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use unstorage for storage abstraction

Files:

  • src/build/types.ts
  • src/types/fetch/fetch.ts
  • src/types/fetch/_match.ts
src/build/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Virtual modules must be registered in src/build/virtual.ts

Files:

  • src/build/types.ts
🧠 Learnings (1)
📚 Learning: 2025-12-24T11:45:17.435Z
Learnt from: CR
Repo: nitrojs/nitro PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-24T11:45:17.435Z
Learning: Applies to src/runtime/**/*.{ts,js} : In runtime code, prefer Web APIs (fetch, Request, Response, URL, etc.) over Node.js APIs

Applied to files:

  • src/types/fetch/fetch.ts
🧬 Code graph analysis (3)
test/fixture/typed-routes.types.ts (1)
src/types/fetch/fetch.ts (3)
  • InternalApi (6-6)
  • TypedInternalResponse (45-59)
  • AvailableRouterMethod (63-70)
src/types/fetch/fetch.ts (1)
src/types/fetch/_match.ts (1)
  • MatchedRouteMethods (134-137)
src/types/fetch/_match.ts (2)
src/presets/cloudflare/wrangler/environment.ts (1)
  • Route (30-34)
src/types/fetch/fetch.ts (1)
  • InternalApi (6-6)
🪛 Biome (2.1.2)
src/types/fetch/fetch.ts

[error] 6-6: An empty interface is equivalent to {}.

Safe fix: Use a type alias instead.

(lint/suspicious/noEmptyInterface)

🔇 Additional comments (12)
src/build/types.ts (2)

111-136: LGTM!

The buildRouteTree function correctly constructs a nested route tree from the flat types.routes map. The handling of the root path / as an empty segments array and the method bucket $ at each leaf node aligns well with the PR's objective of O(path depth) matching.


154-166: Missing semicolons after nested object properties.

Similar to the method entries, the nested segment properties should have trailing semicolons for valid TypeScript interface syntax.

🐛 Proposed fix
     for (const [segment, child] of Object.entries(node)) {
       if (segment === "$" || !child || typeof child !== "object") {
         continue;
       }
       lines.push(`${indent}'${segment}': {`);
       lines.push(
         ...renderRouteTree(
           child as ReturnType<typeof buildRouteTree>,
           level + 1
         )
       );
-      lines.push(`${indent}}`);
+      lines.push(`${indent}};`);
     }

Also update the $ object closing (line 151):

-      lines.push(`${indent}}`);
+      lines.push(`${indent}};`);

Likely an incorrect or invalid review comment.

test/fixture/typed-routes.types.ts (1)

8-31: LGTM!

The module augmentation correctly demonstrates the new nested InternalApi structure with the $ method bucket pattern. The test cases comprehensively cover exact routes, parameterized routes (:id), and catch-all routes (**), which validates the matching logic in _match.ts.

src/types/fetch/fetch.ts (3)

5-6: Empty interface is intentional for module augmentation.

The static analysis tool flagged this as equivalent to {}. However, this empty interface is intentionally designed as an extension point for module augmentation (as demonstrated in the test fixture). This pattern is idiomatic for Nitro's type system. No change needed.


19-29: LGTM!

The RoutePaths type correctly extracts all valid route paths from the nested InternalApi structure. The recursive logic with StripMethodsKey properly excludes the $ method bucket from path segments while still recognizing nodes with methods as valid endpoints.


38-43: LGTM!

The MiddlewareOf and AvailableRouterMethod types correctly use the new MatchedRouteMethods<Route> instead of the previous InternalApi[MatchedRoutes<Route>] pattern. The conditional logic for method extraction and default fallback is preserved correctly.

Also applies to: 62-70

src/types/fetch/_match.ts (6)

3-9: LGTM!

The MatchResult and RouteMethods types provide a clean foundation for the matching system. The exact boolean flag in MatchResult enables prioritization of exact matches, and RouteMethods correctly extracts the method bucket using conditional type inference.


11-29: LGTM!

The path normalization utilities correctly strip query strings, hash fragments, and leading/trailing slashes in the proper order. The NormalizePath composition ensures consistent path handling for matching.


47-57: LGTM!

The ExactMatch type correctly implements strict segment-by-segment matching. It only succeeds when all path segments exist as literal keys in the route tree and the final node contains a $ method bucket.


59-125: LGTM!

The LooseMatch and LooseMatchStep types implement a sophisticated fallback matching algorithm. The approach correctly:

  1. Tracks potential fallback matches (nodes with $ at intermediate levels)
  2. Accumulates catch-all matches as the algorithm descends
  3. Prioritizes exact segment matches over params over catch-alls
  4. Returns a union of all valid matches for TypeScript to resolve

The priority ordering (exact → param → catch-all) aligns with the PR objectives.


127-132: LGTM!

The MatchRoute type correctly orchestrates the matching strategy by trying ExactMatch first and falling back to LooseMatch only when no exact match exists. The [Exact] extends [never] pattern is the idiomatic way to check for the never type in TypeScript.


134-139: LGTM!

The MatchedRouteMethods export provides the new API while MatchedRoutes is retained as an alias for backward compatibility. This is a clean approach that allows gradual migration.

Comment on lines +146 to +152
if (node.$) {
lines.push(`${indent}'$': {`);
for (const [method, types] of Object.entries(node.$)) {
lines.push(`${childIndent}'${method}': ${types.join(" | ")}`);
}
lines.push(`${indent}}`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing semicolon in generated TypeScript code.

The rendered method entries are missing trailing semicolons, which will produce invalid TypeScript syntax in the generated nitro-routes.d.ts file.

🐛 Proposed fix
     if (node.$) {
       lines.push(`${indent}'$': {`);
       for (const [method, types] of Object.entries(node.$)) {
-        lines.push(`${childIndent}'${method}': ${types.join(" | ")}`);
+        lines.push(`${childIndent}'${method}': ${types.join(" | ")};`);
       }
       lines.push(`${indent}}`);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (node.$) {
lines.push(`${indent}'$': {`);
for (const [method, types] of Object.entries(node.$)) {
lines.push(`${childIndent}'${method}': ${types.join(" | ")}`);
}
lines.push(`${indent}}`);
}
if (node.$) {
lines.push(`${indent}'$': {`);
for (const [method, types] of Object.entries(node.$)) {
lines.push(`${childIndent}'${method}': ${types.join(" | ")};`);
}
lines.push(`${indent}}`);
}
🤖 Prompt for AI Agents
In @src/build/types.ts around lines 146 - 152, The generated method entries for
node.$ are missing trailing semicolons; update the code that pushes method lines
(the loop using lines.push(`${childIndent}'${method}': ${types.join(" | ")}`))
to append a semicolon so each entry becomes `${childIndent}'${method}':
${types.join(" | ")};`, ensuring valid TypeScript in the emitted declarations.
Ensure the change is applied where indent/childIndent and node.$ are handled so
all rendered method lines include the semicolon.

Comment on lines +171 to +185
const generateRoutes = () => {
const routeTree = buildRouteTree();
return [
"// Generated by nitro",
'import type { Serialize, Simplify } from "nitro/types";',
'declare module "nitro/types" {',
" type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T",
" interface InternalApi {",
...renderRouteTree(routeTree, 2),
" }",
"}",
// Makes this a module for augmentation purposes
"export {}",
];
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to see the full context
head -200 src/build/types.ts | tail -100

Repository: nitrojs/nitro

Length of output: 2816


🏁 Script executed:

# Check TypeScript version requirements in package.json
cat package.json | grep -A 5 '"typescript"'

Repository: nitrojs/nitro

Length of output: 207


🏁 Script executed:

# Check tsconfig.json for TypeScript version target
cat tsconfig.json | grep -E '(compilerOptions|target)' -A 5

Repository: nitrojs/nitro

Length of output: 254


🏁 Script executed:

# Search for any comments or documentation about the Awaited type
rg "Awaited" --context 5

Repository: nitrojs/nitro

Length of output: 2411


Remove redundant Awaited type declaration.

The local Awaited type at line 177 shadows TypeScript's built-in utility type. Since the project requires TypeScript ^5.9.3 (which includes the built-in Awaited), this local redeclaration is unnecessary and should be removed. The type will remain available within the module augmentation scope without the redundant definition.

🤖 Prompt for AI Agents
In @src/build/types.ts around lines 171 - 185, The generateRoutes function
currently injects a local Awaited type declaration that shadows TypeScript's
built-in Awaited; remove the redundant "type Awaited..." line from the generated
output in generateRoutes so the module augmentation relies on the built-in
utility instead—update generateRoutes (which builds routeTree via buildRouteTree
and renders with renderRouteTree) to omit the manual Awaited declaration while
keeping the rest of the declared module and exported empty module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant