Skip to content

fix(nest): route non-identifier path params and reject literal colons - #2207

Closed
dinwwwh wants to merge 2 commits into
mainfrom
claude/exciting-bohr-x9ghpg
Closed

dinwwwh wants to merge 2 commits into
mainfrom
claude/exciting-bohr-x9ghpg

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 12, 2026

Copy link
Copy Markdown
Member

Problem

@Implement turned every {param} into :<name> and copied literal text unchanged. That misroutes two kinds of path on both the Express and Fastify adapters (reproduced on Nest 12):

  • Param names that aren't identifiers. Both routers end :name at the first -, so for /users/{user-id}, GET /users/42 returned 404, while GET /users/42-id matched with params { user: '42' }. A name starting with a digit ({1st}) made Express throw at startup.
  • A literal : or *. /models:predict was registered as /models plus a param named predict, so POST /modelsXYZ ran with input { predict: 'XYZ' }.

Changes

  • A new helper, toNestRoute(path), returns { pattern, paramNames }. Both toNestRouteDecorator and toORPCOpenAPIParams use it, so building the route and decoding its params can't drift apart.
    • A non-rest param whose name doesn't match /^[a-z_]\w*$/i gets a generated key (orpc_p<index>), with _ prepended if a param in the same path already has that name. Decoding maps the key back to the original name.
    • Identifier-safe names stay as they are, so guards and pipes that read req.params.id keep working. Controller-level params like @Controller('/:tenant') and other unknown keys pass through unchanged. The rest param is still read from path (Express) or * (Fastify).
  • A literal : or * anywhere in the full path (prefix included, outside a param) now throws a descriptive TypeError when @Implement is applied, for procedure and router contracts alike. It can't be escaped portably: Express needs \:, Fastify needs ::, and Nest's Express adapter throws on ::.
  • Docs: the "Define Your Contract" section of integrations/nest.mdx now has a warning about the literal :/* limitation and a note that non-identifier params appear under generated names in req.params. The @Implement JSDoc mentions the limitation too.

Tests

Added to packages/nest/src/implement.test.ts. The routing cases run on both adapters.

  • {user-id} routes correctly: /users/42 gives { 'user-id': '42' } and /users/42-id gives { 'user-id': '42-id' }. {1st} also routes.
  • A path that mixes a prefix param, a param literally named orpc_p0 and two non-identifier params maps every value back to the right name.
  • A hyphenated param under @Controller('/:tenant') still passes tenant through.
  • Literal : or * (plain, inside a non-param {user:id} segment, or in the prefix) throws for both procedure and router contracts.

All 8 new tests fail on main; with {1st} removed, since it crashes Express at startup. They pass with this change. Removing the collision loop makes the mixed-params test fail.

Validation: pnpm vitest run packages/nest (110 passed), pnpm --filter @orpc/nest type:check, pnpm lint and pnpm docs:validate all pass.

Note

Another task fixes non-trailing {+rest} params in the same function, which this PR replaces (toNestPattern → toNestRoute). I couldn't find an open PR for it, so whichever lands second needs a rebase. The rest-param branch inside toNestRoute is where that change belongs. For reference, a path with more than one {+rest} param already fails at startup on both adapters, before and after this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wd5XPg3bL7UKDGdi4akyu6


Generated by Claude Code

`@Implement` wrote `:<name>` for every path param and copied literal text
verbatim, so on both the Express and Fastify adapters:

- params like `{user-id}` ended at the `-` (`GET /users/42` was a 404 while
  `GET /users/42-id` matched with `{ user: '42' }`), and digit-leading names
  like `{1st}` made Express throw at startup;
- a literal `:` or `*` (e.g. `/models:predict`) was parsed as route syntax,
  so `POST /modelsXYZ` was handled with input `{ predict: 'XYZ' }`.

Build the Nest route with a single `toNestRoute` helper shared by route
registration and param decoding. Params whose names are not identifiers are
registered under generated keys (avoiding names already used in the path)
and mapped back to their original names when decoding; identifier-safe
names and controller-level params are unchanged. Literal `:` / `*` can't be
escaped portably (Express needs `\:`, Fastify `::`, which Express rejects),
so `@Implement` now throws a descriptive TypeError at decoration time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wd5XPg3bL7UKDGdi4akyu6
@pkg-pr-new

pkg-pr-new Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@2207

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@2207

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@2207

@orpc/client

npm i https://pkg.pr.new/@orpc/client@2207

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@2207

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@2207

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@2207

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@2207

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@2207

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@2207

@orpc/experimental-lock

npm i https://pkg.pr.new/@orpc/experimental-lock@2207

@orpc/experimental-msw

npm i https://pkg.pr.new/@orpc/experimental-msw@2207

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@2207

@orpc/next

npm i https://pkg.pr.new/@orpc/next@2207

@orpc/node

npm i https://pkg.pr.new/@orpc/node@2207

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@2207

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@2207

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@2207

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@2207

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@2207

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@2207

@orpc/server

npm i https://pkg.pr.new/@orpc/server@2207

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@2207

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@2207

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@2207

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@2207

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@2207

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@2207

commit: 3824720

@codecov

codecov Bot commented Oct 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

⚠️ 5 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 28 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing claude/exciting-bohr-x9ghpg (3824720) with main (5f441da)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • toNestRoute helper — replaces toNestPattern, returning { pattern, paramNames }; both route registration and param decoding call it, so they cannot drift.
  • Non-identifier param names — names failing /^[a-z_]\w*$/i are registered under a generated orpc_p<index> key (with _ prefix on collision) and mapped back to the original name during decoding; identifier-safe names stay :<name>.
  • Literal : / * rejection — toNestLiteral throws a TypeError at decoration time for procedure and router contracts when the merged path (prefix included) contains a literal : or *.
  • Decode rewrite — toORPCOpenAPIParams maps generated keys and the rest key (* Fastify / path Express) back to original names, passing controller-level keys through unchanged.
  • Docs + JSDoc — warning on the literal :/* limitation and note on generated req.params names in integrations/nest.mdx, plus the @Implement remark.
  • Tests — throw cases for both contract kinds and cross-adapter routing cases for {user-id}, {1st}, and the mixed-prefix collision path.

I verified the decode/route logic against getDynamicPathParams (packages/openapi/src/utils.ts, name regex [\w-]+) and mergeHttpPath, and ran pnpm vitest run packages/nest (110 passed), type:check, and eslint on the changed files — all clean. The collision loop is sound: generated keys are unique per index and the _-prefix loop terminates. The intended limitations called out in the description (multiple {+rest} params, non-trailing {+rest}) are pre-existing and out of scope.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
orpc 3824720 Commit Preview URL

Branch Preview URL
Oct 12 2026, 10:11 AM

Both call sites merged `meta.prefix` before calling `toNestRoute`. Since the
generated param keys are indexed across prefix + path, the decorator and the
param decoder must see the exact same full path; doing the merge in one place
keeps them from drifting. Also clarify the docs note on `req.params`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wd5XPg3bL7UKDGdi4akyu6

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

The follow-up commit (3824720) is a behavior-preserving refactor of the route helper — routing and param decoding are unchanged.

  • Prefix merged inside toNestRoute — the helper now takes (path, prefix?) and merges internally, replacing the duplicated meta.prefix ? mergeHttpPath(meta.prefix, meta.path) : meta.path at both call sites (toNestRouteDecorator and toORPCOpenAPIParams). Argument order to mergeHttpPath is preserved, so the resulting pattern and paramNames are identical.
  • JSDoc + docs wording — the helper doc now mentions the optional prefix, and the req.params info callout in nest.mdx was reworded so guards/pipes are described as seeing the generated names.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@Mnigos Mnigos left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tried the pkg.pr.new build on Nest 11.2.7 with Express and with a Hono adapter, fix works on both. Three small things:

  1. On adapters that already accept :user-id as a label (Hono does), guards used to see req.params['user-id'] and now see orpc_p0. Worth noting in the docs that the rename happens regardless of adapter.
  2. /dot/{user.id} isn't a valid param, so {user.id} goes through literally and Express treats {…} as an optional group: /dot/42 is 404, /dot/ and /dot/user.id are 200. If toNestLiteral rejects : and *, it could reject stray {/} too.
  3. toNestRoute now runs on every request in toORPCOpenAPIParams. A per-contract WeakMap cache would keep the hot path cheap.

@dinwwwh dinwwwh closed this Oct 13, 2026
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.

3 participants