Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

OpenAPI Link

Use OpenAPILink to call HTTP endpoints served by OpenAPI Handler and other OpenAPI-compliant servers.

Overview

const link = new OpenAPILink(contract, {
  origin: 'https://api.example.com',
  url: '/api',
  headers: ({ context }) => ({
    authorization: context?.token ? `Bearer ${context.token}` : undefined,
  }),
  interceptors: [
    async ({ next, path }) => {
      console.time(path.join('.'))

      try {
        return await next()
      }
      finally {
        console.timeEnd(path.join('.'))
      }
    },
  ],
  plugins: [
    new RetryAfterLinkPlugin(),
  ],
  fetch: (request, init) => { // <- only available in fetch adapter
    return globalThis.fetch(request, {
      ...init,
      credentials: 'include', // Include cookies on cross-origin requests
    })
  },
})

Typesafe Clients

After you create an OpenAPILink, pass it to createORPCClient to build a typesafe client for either a contract or a router:

import { createORPCClient } from '@orpc/client'
import { RouterContractClient } from '@orpc/contract'
import { JsonifiedClient } from '@orpc/openapi'
import { RouterClient } from '@orpc/server'

// if you are following contract-first approach
const contractClient: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(link)

// if you are following normal approach
const routerClient: JsonifiedClient<RouterClient<typeof router>> = createORPCClient(link)

Client Context

Client context lets you pass per-call values, such as auth tokens or cache hints. This context is available in link options, interceptors, plugins, and other extensibility points.

type ClientContext = {
  token?: string
}

const link = new OpenAPILink<ClientContext>(contract, {
  headers: ({ context }) => ({
    authorization: context?.token ? `Bearer ${context.token}` : undefined,
  }),
})

URL and Header Options

Use origin, url, and headers to control request destination and headers.

  • origin: Server protocol and domain. Omit in the browser to use the current origin.
  • url: Usually a path prefix like /api. May include query params that are added to every request.
  • headers: Headers sent with every request, such as auth or trace IDs. Keys should be lowercase.
const link = new OpenAPILink(contract, {
  origin: 'https://api.example.com',
  url: '/api?v=2',
  headers: {
    authorization: `Bearer ${getAuthToken()}`,
  },
})

Interceptors

Interceptors let you observe or customize different stages of an OpenAPI call. Common use cases include logging, retries, auth, batching, and transport customization.

Interceptors

Interceptors run around the entire call, including input encoding, transport, and response decoding. Use them when you need access to the path, input, output, or error.

const link = new OpenAPILink(contract, {
  interceptors: [
    async ({ next, path, input }) => {
      console.time(path.join('.'))

      try {
        const output = await next()
        return output
      }
      catch (err) {
        console.error(`${path.join('.')}:`, err)
        throw err
      }
      finally {
        console.timeEnd(path.join('.'))
      }
    },
  ],
})

Transport Interceptors

Interceptors run after input encoding and before response decoding. Use them to inspect or rewrite the request.

const link = new OpenAPILink(contract, {
  transportInterceptors: [
    async (options) => {
      const response = await options.next({
        ...options,
        request: {
          ...options.request,
          headers: {
            ...options.request.headers,
            'x-request-id': crypto.randomUUID(),
          },
        },
      })

      return response
    },
  ],
})

Adapter Interceptors

Some OpenAPILink implementations also support adapter-specific interceptors. The fetch adapter exposes fetchInterceptors, which run right before fetch and give you access to the final url and RequestInit.

const link = new OpenAPILink(contract, {
  fetchInterceptors: [
    async (options) => {
      const response = await options.next({
        ...options,
        init: {
          ...options.init,
          credentials: 'include',
        },
      })

      return response
    },
  ],
})

Plugins

Plugins package reusable interceptors. For example, Retry After Plugin adds retry behavior based on the retry-after response header.

const link = new OpenAPILink(contract, {
  plugins: [
    new RetryAfterLinkPlugin(),
  ],
})

Custom Serializer

Provide a custom serializer when you need to extend or override the default serialization behavior. For more details, see OpenAPI Serializer.

const link = new OpenAPILink(contract, {
  serializer: new OpenAPISerializer({
    handlers: {
      // ...custom handlers
    },
  }),
})

Custom Error Decoding

If your server returns error responses that don’t match oRPC’s expected format, use customErrorResponseBodyDecoder to customize the decoding logic. This works together with Custom Error Response on the server.

const link = new OpenAPILink(contract, {
  customErrorResponseBodyDecoder: (body, response) => {
    if (response.status === 422 && typeof body === 'object' && body && 'detail' in body) {
      return new ORPCError('BAD_REQUEST', {
        message: String(body.detail),
      })
    }

    // fallback to default error decoding logic by returning null or undefined
    return null
  },
})

Event Stream Options

Configure how an AsyncIteratorObject is streamed to the server. Available options depend on the adapter. For example, the fetch adapter supports:

const link = new OpenAPILink(contract, {
  toFetchRequest: {
    eventStream: {
      initialComment: {
        /**
         * If true, an initial comment is sent immediately upon stream start to flush headers.
         * This allows the receiving side to establish the connection without waiting for the first event.
         *
         * @default true
         */
        enabled: true,
        /**
         * The content of the initial comment sent upon stream start. Must not include newline characters.
         *
         * @default ''
         */
        comment: '',
      },
      keepAlive: {
        /**
         * If true, a ping comment is sent periodically to keep the connection alive.
         *
         * @default true
         */
        enabled: true,
        /**
         * Interval (in milliseconds) between ping comments sent after the last event.
         *
         * @default 15000
         */
        interval: 15000,
        /**
         * The content of the ping comment. Must not include newline characters.
         *
         * @default ''
         */
        comment: '',
      },
      /**
       * If true, a `close` event is sent even when the iterator completes with `undefined`.
       * When the iterator returns a value, a `close` event is always emitted regardless of this setting.
       *
       * @default true
       */
      emptyCloseEventEnabled: true,
    },
  },
})

Lifecycle

TODO: add lifecycle diagram

Last updated on August 6, 2026

Was this page helpful?