Skip to content

docs(openapi): document OpenAPI path parameter syntax requirement ({i… - #1808

Closed
Wadiou wants to merge 1 commit into
middleapi:mainfrom
Wadiou:docs/openapi-path-parameter-syntax
Closed

docs(openapi): document OpenAPI path parameter syntax requirement ({i…#1808
Wadiou wants to merge 1 commit into
middleapi:mainfrom
Wadiou:docs/openapi-path-parameter-syntax

Conversation

@Wadiou

@Wadiou Wadiou commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

docs(openapi): document OpenAPI path parameter syntax requirement (/{id} vs /:id)

Summary

  • Added ### Path parameter syntax ({id} vs :id) section in apps/content/docs/migrations/from-v1.mdx with side-by-side v2 vs v1 <CodeGroup> comparisons.
  • Added a warning callout in apps/content/docs/openapi/routing.mdx under ## Path Parameters.

These updates clarify that oRPC v2 strictly requires OpenAPI curly brace syntax (/{id}). Express-style colon parameters (/:id) are treated as literal static paths and do not extract path parameters into procedure input (422 INPUT_VALIDATION_FAILED).

…d} vs :id)

Add a `### Path parameter syntax ({id} vs :id)` section to `from-v1.mdx` with side-by-side v2 vs v1 CodeGroup examples, and a warning callout in `openapi/routing.mdx`. This clarifies that OpenAPI curly brace syntax (`/{id}`) is required in v2, while Express-style colon parameters (`/:id`) are treated as static literal paths.
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview Aug 7, 2026 3:38pm

@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

  • apps/content/docs/migrations/from-v1.mdx: added a ### Path parameter syntax (\/{id}` vs `/:id`)subsection with v2/v1examples under## Routing Moved to OpenAPI Metadata`.
  • apps/content/docs/openapi/routing.mdx: added a :::warning callout under ## Path Parameters clarifying that only curly-brace syntax is supported.

I verified the behavioral claims against the implementation: getDynamicPathParams in packages/openapi/src/utils.ts only recognizes segments wrapped in {...} (char codes 123/125), so Express-style :name segments are indeed treated as literal static paths and never extracted into procedure input. The wording is accurate, both files use established doc conventions (:::warning and <CodeGroup> are used throughout the docs), and the new subsection is placed under a sensible heading. Mergeable as-is.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh

dinwwwh commented Aug 8, 2026

Copy link
Copy Markdown
Member

We don't recommend using the /:id pattern. It isn't documented in v1 or v2, but it still works in v2. please provide minimal repro for validation failed when use :id

@Wadiou

Wadiou commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Reproduction: Path Parameter Syntax (/:id vs /{id})

package.json

{
  "name": "orpc-path-syntax-repro",
  "private": true,
  "type": "module",
  "dependencies": {
    "@orpc/contract": "2.0.0-beta.25",
    "@orpc/openapi": "2.0.0-beta.25",
    "@orpc/server": "2.0.0-beta.25",
    "@orpc/zod": "2.0.0-beta.25",
    "zod": "^4.3.5"
  }
}

repro.ts

import { oc } from '@orpc/contract'
import { implement } from '@orpc/server'
import { openapi, OpenAPIGenerator } from '@orpc/openapi'
import { OpenAPIMatcher, OpenAPILinkCodec } from '@orpc/openapi/standard'
import { ZodToJsonSchemaConverter } from '@orpc/zod'
import { z } from 'zod'

// Contract Definitions
const contractColon = oc.meta(openapi({ method: 'GET', path: '/users/:id' })).input(z.object({ id: z.string() }))
const contractBrace = oc.meta(openapi({ method: 'GET', path: '/users/{id}' })).input(z.object({ id: z.string() }))

// Procedure Implementations
const procedureColon = implement(contractColon).handler(({ input }) => ({ id: input.id }))
const procedureBrace = implement(contractBrace).handler(({ input }) => ({ id: input.id }))

const generator = new OpenAPIGenerator({ converters: [new ZodToJsonSchemaConverter()] })

// 1. Direct Server Route Matching
console.log('--- 1. Direct Server Route Matching ---')
const matcherColon = new OpenAPIMatcher({ users: { findOne: procedureColon } })
const matchColon = await matcherColon.match('GET', '/users/usr_123', undefined)
console.log('Server match for /users/:id  -> params:', matchColon?.params)

// 2. Client Link Codec URL Encoding
console.log('\n--- 2. Client Link Codec ---')
const linkCodecColon = new OpenAPILinkCodec({ users: { findOne: contractColon } })
const linkCodecBrace = new OpenAPILinkCodec({ users: { findOne: contractBrace } })

const reqColon = await linkCodecColon.encodeInput({ id: 'usr_123' }, ['users', 'findOne'], {} as any)
const reqBrace = await linkCodecBrace.encodeInput({ id: 'usr_123' }, ['users', 'findOne'], {} as any)

console.log('Client URL for /users/:id   ->', reqColon.url) // BROKEN:  /users/:id?id=usr_123
console.log('Client URL for /users/{id}  ->', reqBrace.url) // CORRECT: /users/usr_123

// 3. OpenAPI Spec Generator
console.log('\n--- 3. OpenAPI Document Generator ---')
const spec = await generator.generate({
  colonRoute: procedureColon,
  braceRoute: procedureBrace,
})

console.log('Spec param for /users/:id  -> in:', spec.paths?.['/users/:id']?.get?.parameters?.[0]?.in) // BROKEN:  'query'
console.log('Spec param for /users/{id} -> in:', spec.paths?.['/users/{id}']?.get?.parameters?.[0]?.in) // CORRECT: 'path'

Console Output

--- 1. Direct Server Route Matching ---
Server match for /users/:id  -> params: { id: 'usr_123' }

--- 2. Client Link Codec ---
Client URL for /users/:id   -> /users/:id?id=usr_123
Client URL for /users/{id}  -> /users/usr_123

--- 3. OpenAPI Document Generator ---
Spec param for /users/:id  -> in: query
Spec param for /users/{id} -> in: path

@dinwwwh

@dinwwwh

dinwwwh commented Aug 8, 2026

Copy link
Copy Markdown
Member

Does the code above actually work in v1?

https://github.com/middleapi/orpc/blob/1.x/packages/openapi-client/src/adapters/standard/utils.ts#L13
https://github.com/middleapi/orpc/blob/1.x/packages/openapi-client/src/adapters/standard/openapi-link-codec.ts#L90

In v1, I don't think it should work either. I mean, there is only partial support for the :id pattern in the handler; the rest isn't supported, and there is no official documentation for this support.

@Wadiou

Wadiou commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Minimal Reproduction (:id vs {id} in oRPC v1 vs v2)

1. oRPC v1 Reproduction

package.json

{
  "name": "repro-v1",
  "private": true,
  "type": "module",
  "dependencies": {
    "@nestjs/common": "^11.0.1",
    "@nestjs/core": "^11.0.1",
    "@nestjs/platform-fastify": "^11.0.1",
    "@orpc/contract": "1.14.15",
    "@orpc/nest": "1.14.15",
    "@orpc/openapi": "1.14.15",
    "@orpc/server": "1.14.15",
    "@orpc/zod": "1.14.15",
    "fastify": "^5.2.0",
    "reflect-metadata": "^0.2.2",
    "zod": "^3.23.8"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true
  }
}

server.ts

import 'reflect-metadata'
import { Controller, Module } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'
import { oc } from '@orpc/contract'
import { Implement, ORPCModule } from '@orpc/nest'
import { implement } from '@orpc/server'
import { z } from 'zod'

console.log('=== oRPC v1 NestJS Backend Reproduction ===\n')

const contractColon = oc.route({ method: 'GET', path: '/users-colon/:id' }).input(z.object({ id: z.string() }))
const contractBrace = oc.route({ method: 'GET', path: '/users-brace/{id}' }).input(z.object({ id: z.string() }))

@Controller()
class UsersController {
  @Implement(contractColon)
  findOneColon() {
    return implement(contractColon).handler(async ({ input }) => {
      return { id: input.id }
    })
  }

  @Implement(contractBrace)
  findOneBrace() {
    return implement(contractBrace).handler(async ({ input }) => {
      return { id: input.id }
    })
  }
}

@Module({
  imports: [ORPCModule.forRoot({})],
  controllers: [UsersController],
})
class AppModule {}

async function main() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
    { logger: false }
  )

  await app.listen({ port: 3001 })
  console.log('v1 NestJS Fastify Server listening on http://localhost:3001\n')

  const resColon = await fetch('http://localhost:3001/users-colon/usr_123')
  console.log('v1 HTTP GET /users-colon/:id  -> Status:', resColon.status)
  console.log('Body:', await resColon.json())

  const resBrace = await fetch('http://localhost:3001/users-brace/usr_123')
  console.log('\nv1 HTTP GET /users-brace/{id} -> Status:', resBrace.status)
  console.log('Body:', await resBrace.json())

  await app.close()
}

main()

Console Output (npx tsx server.ts)

=== oRPC v1 NestJS Backend Reproduction ===

v1 NestJS Fastify Server listening on http://localhost:3001

v1 HTTP GET /users-colon/:id  -> Status: 200
Body: { id: 'usr_123' }

v1 HTTP GET /users-brace/{id} -> Status: 200
Body: { id: 'usr_123' }

2. oRPC v2 Reproduction

package.json

{
  "name": "repro-v2",
  "private": true,
  "type": "module",
  "dependencies": {
    "@nestjs/common": "^11.0.1",
    "@nestjs/core": "^11.0.1",
    "@nestjs/platform-fastify": "^11.0.1",
    "@orpc/contract": "2.0.0-beta.25",
    "@orpc/nest": "2.0.0-beta.25",
    "@orpc/openapi": "2.0.0-beta.25",
    "@orpc/server": "2.0.0-beta.25",
    "@orpc/zod": "2.0.0-beta.25",
    "fastify": "^5.2.0",
    "reflect-metadata": "^0.2.2",
    "zod": "^4.3.5"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true
  }
}

server.ts

import 'reflect-metadata'
import { Controller, Module } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'
import { oc } from '@orpc/contract'
import { Implement, ORPCModule } from '@orpc/nest'
import { openapi } from '@orpc/openapi'
import { implement } from '@orpc/server'
import { z } from 'zod'

console.log('=== oRPC v2 NestJS Backend Reproduction ===\n')

const contractColon = oc.meta(openapi({ method: 'GET', path: '/users-colon/:id' })).input(z.object({ id: z.string() }))
const contractBrace = oc.meta(openapi({ method: 'GET', path: '/users-brace/{id}' })).input(z.object({ id: z.string() }))

@Controller()
class UsersController {
  @Implement(contractColon)
  findOneColon() {
    return implement(contractColon).handler(async ({ input }) => {
      return { id: input.id }
    })
  }

  @Implement(contractBrace)
  findOneBrace() {
    return implement(contractBrace).handler(async ({ input }) => {
      return { id: input.id }
    })
  }
}

@Module({
  imports: [ORPCModule.forRoot({})],
  controllers: [UsersController],
})
class AppModule {}

async function main() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
    { logger: false }
  )

  await app.listen({ port: 3002 })
  console.log('v2 NestJS Fastify Server listening on http://localhost:3002\n')

  const resColon = await fetch('http://localhost:3002/users-colon/usr_123')
  console.log('v2 HTTP GET /users-colon/:id  -> Status:', resColon.status)
  console.log('Body:', await resColon.json())

  const resBrace = await fetch('http://localhost:3002/users-brace/usr_123')
  console.log('\nv2 HTTP GET /users-brace/{id} -> Status:', resBrace.status)
  console.log('Body:', await resBrace.json())

  await app.close()
}

main()

Console Output (npx tsx server.ts)

=== oRPC v2 NestJS Backend Reproduction ===

v2 NestJS Fastify Server listening on http://localhost:3002

v2 HTTP GET /users-colon/:id  -> Status: 400
Body: {
  defined: false,
  inferable: false,
  code: 'BAD_REQUEST',
  message: 'Input validation failed',
  data: { issues: [ [Object] ] }
}

v2 HTTP GET /users-brace/{id} -> Status: 200
Body: { id: 'usr_123' }

@Wadiou

Wadiou commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@dinwwwh i managed to create the exact issue i was having in my project

@dinwwwh

dinwwwh commented Aug 8, 2026

Copy link
Copy Markdown
Member

Should fix in #1818 but there no reason to keep using :id

@Wadiou

Wadiou commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

ofc i won't use it , it was added by the llm the first time i added orpc so when it was working fine i didn't pay attention to it

anyways thanks for your attention

@dinwwwh dinwwwh closed this Aug 9, 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.

2 participants