Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

OpenAPI Routing

Use openapi metadata to control how a procedure is exposed over HTTP.

Basic Routing

If you do not set OpenAPI routing metadata, a procedure is exposed as a POST endpoint whose path is derived from the router structure. For example:

import { 
module openapi
const openapi: OpenAPIFunction
Creates OpenAPI meta plugins that control how a procedure is exposed over HTTP, such as its method, path, prefix, and OpenAPI operation spec.
@see{@link https://orpc.dev/docs/openapi/routing OpenAPI Routing}@see{@link https://orpc.dev/docs/openapi/specification OpenAPI Specification}
openapi
} from '@orpc/openapi'
const
const router: {
    planet: {
        list: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
            id: string;
            name: string;
        }[]>, Record<never, never>, never>;
        create: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{}>, Record<never, never>, never>;
    };
}
router
= {
planet: {
    list: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
        id: string;
        name: string;
    }[]>, Record<never, never>, never>;
    create: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{}>, Record<never, never>, never>;
}
planet
: {
list: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
    id: string;
    name: string;
}[]>, Record<never, never>, never>
list
: const os: Builder<DefaultInitialContext & object, Record<never, never>>
The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler` to define procedures, then compose them into routers.
@see{@link https://orpc.dev/docs/procedure Procedure}
os
.Builder<DefaultInitialContext & object, Record<never, never>>.meta(...plugins: MetaPlugin<InitialInputSchema, InitialOutputSchema, Record<never, never>>[]): Builder<DefaultInitialContext & object, Record<never, never>>meta(function openapi(meta: OpenAPIMeta): OpenAPIMetaPlugin<any, any, any>
Creates OpenAPI meta plugins that control how a procedure is exposed over HTTP, such as its method, path, prefix, and OpenAPI operation spec.
@see{@link https://orpc.dev/docs/openapi/routing OpenAPI Routing}@see{@link https://orpc.dev/docs/openapi/specification OpenAPI Specification}
openapi
({ OpenAPIMeta.method?: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | undefined
HTTP method accepted by this procedure.
@default'POST'
method
: 'GET', OpenAPIMeta.path?: `/${string}` | undefined
URL path for this procedure. Supports dynamic parameters via `{param}` syntax, and `{+param}` to allow slashes in the matched value.
@example`/users`, `/users/{id}`, `/files/{+path}`@defaultRouter segments joined by `'/`
path
: '/planets' }))
.
Builder<DefaultInitialContext & object, Record<never, never>>.handler<{
    id: string;
    name: string;
}[]>(handler: ProcedureHandler<DefaultInitialContext & object, unknown, {
    id: string;
    name: string;
}[], ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
    id: string;
    name: string;
}[]>, Record<never, never>, never>
handler
(async () => [{ id: stringid: 'earth', name: stringname: 'Earth' }]),
create: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{}>, Record<never, never>, never>create: const os: Builder<DefaultInitialContext & object, Record<never, never>>
The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler` to define procedures, then compose them into routers.
@see{@link https://orpc.dev/docs/procedure Procedure}
os
.Builder<DefaultInitialContext & object, Record<never, never>>.handler<{}>(handler: ProcedureHandler<DefaultInitialContext & object, unknown, {}, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{}>, Record<never, never>, never>handler(async () => ({})), } }

In this example, list is exposed as GET /planets because it overrides the default method and path. create keeps the default behavior, so it is exposed as POST /planet/create.

Path Parameters

To define a path parameter, use {name} in the path and add the same field as a required key in the input schema:

import { z } from 'zod'

const getPlanet = os
  .meta(openapi({ method: 'GET', path: '/planets/{id}' }))
  .input(z.object({ id: z.string() }))

For catch-all path segments that may include /, use {+name}:

const getFile = os
  .meta(openapi({ method: 'GET', path: '/files/{+path}' }))
  .input(z.object({ path: z.string() }))

Prefixes

Define prefix to prepend a path to a procedure, or an entire router:

const planetBuilder = os.meta(openapi({ prefix: '/planets' }))

const listPlanets = planetBuilder
  .meta(openapi({ method: 'GET', path: '/' }))
  .handler(async () => [{ id: 'earth', name: 'Earth' }])

const createPlanet = planetBuilder
  .handler(async () => ({}))

const router = os.meta(openapi({ prefix: '/api/v2' })).router({
  planet: {
    list: listPlanets,
    create: createPlanet,
  },
})

In this example, listPlanets is exposed as GET /api/v2/planets/. createPlanet is exposed as POST /api/v2/planets/planet/create.

Path Parameters in Prefixes

Prefixes can also include path parameters, but they must be defined as required fields in the input schema.

const base = os
  .meta(openapi({ prefix: '/{workspaceId}' }))
  .input(z.looseObject({ workspaceId: z.string() }))
  .use(({ next }, { workspaceId }) => {
    console.log('Workspace ID:', workspaceId)
    return next()
  })

const procedure = base
  .meta(openapi({ method: 'GET', path: '/planets/{id}' }))
  .input(z.looseObject({ id: z.string() }))
  .handler(async ({ input }) => {
    console.log('Workspace ID:', input.workspaceId)
    console.log('Planet ID:', input.id)
  })

Lazy Router

When using a lazy router, define a prefix so lazy loading is triggered only for relevant requests:

const router = {
  project: os
    .meta(openapi({ prefix: '/projects' }))
    .lazy(() => import('./project')),
}

Metadata Merging

When openapi is applied multiple times, prefix values are concatenated in definition order, while method, path, and successStatus are overridden by the most recent call. For the full merge behavior of every field, see Metadata Merging.

const router = os
  .meta(openapi({ prefix: '/api/v2' }))
  .router({
    get: os
      .meta(openapi({ prefix: '/planets' }))
      .meta(openapi({ method: 'GET', path: '/planets/{id}' }))
      .meta(openapi({ path: '/{id}' }))
      .input(z.object({ id: z.string() }))
      .handler(async () => ({})),
  })

These calls are equivalent to:

const router = {
  get: os
    .meta(openapi({
      prefix: '/api/v2/planets',
      method: 'GET',
      path: '/{id}',
    }))
    .handler(async () => ({})),
}

Shorthands

For common cases, use the shorthand helpers:

const listPlanets = os
  .meta(openapi.prefix('/planets'))
  .meta(openapi.method('GET'))
  .meta(openapi.path('/'))

.route extension

Import @orpc/openapi/extensions/route from a module that always runs during initialization, such as the file where you define your base builder or create your server. This adds a .route method to the builder, allowing you to define OpenAPI metadata directly without wrapping it in .meta(openapi(...)).

const ping = base
  .route({
    method: 'GET',
    path: '/ping',
  })
  .input(z.object({ name: z.string(), }))
  .handler(async ({ input }) => {
    return `Hello ${input.name}!`
  })
import '@orpc/openapi/extensions/route'

import { os } from '@orpc/server'

export const base = os

Last updated on August 6, 2026

Was this page helpful?