Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

OpenAPI Handler

Use OpenAPIHandler to expose HTTP endpoints or communicate with OpenAPI Link and other OpenAPI-compliant clients.

Overview

const handler = new OpenAPIHandler(router, {
  interceptors: [
    async ({ next, path }) => {
      console.time(path.join('.'))

      try {
        return await next()
      }
      catch (err) {
        console.error(`${path.join('.')}:`, err)
        throw err
      }
      finally {
        console.timeEnd(path.join('.'))
      }
    }
  ],
  plugins: [
    new CORSHandlerPlugin()
  ],
})

Interceptors

Interceptors let you observe or change different stages of an OpenAPI request. Common use cases include logging, error handling, and metrics.

Routing Interceptors

Routing interceptors run on every request before routing. Use them when you need to handle all requests, including requests that do not match a procedure.

const handler = new OpenAPIHandler(router, {
  routingInterceptors: [
    async ({ next, request, context }) => {
      if (condition) {
        return { matched: false }
      }

      const { matched, response } = await next()
      return { matched, response }
    },
  ],
})

Interceptors

These interceptors run only for matched requests, after routing and before error handling (but can’t use ORPCError for typesafe errors). Use them when you need access to the matched procedure.

const handler = new OpenAPIHandler(router, {
  interceptors: [
    async ({ next, request, procedure, context }) => {
      try {
        const response = await next()
        return response
      }
      catch (err) {
        if (err instanceof CustomError) {
          throw new ORPCError('CUSTOM_ERROR', { message: err.message, cause: err })
        }

        throw err
      }
    },
    async ({ next, path }) => {
      console.time(path.join('.'))

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

Client Interceptors

Client interceptors run only for matched requests, after input decoding, before output encoding and can use ORPCError for typesafe errors. Use them when you need access to the procedure, input, and output.

const handler = new OpenAPIHandler(router, {
  clientInterceptors: [
    async ({ next, input, context, procedure }) => {
      const output = await next()
      return output
    },
  ],
})

Adapter Interceptors

Some OpenAPIHandler implementations, such as fetch or node adapters, also support adapter interceptors. These run before Routing Interceptors and let you work with the adapter’s native request and response objects.

const handler = new OpenAPIHandler(router, {
  fetchInterceptors: [
    async ({ next, request }) => {
      const { matched, response } = await next()
      return { matched, response }
    },
  ],
})

Plugins

Plugins package reusable interceptors. For example, CORS Plugin adds a routing interceptor to handle preflight requests and adds CORS headers to every response.

const handler = new OpenAPIHandler(router, {
  plugins: [
    new CORSHandlerPlugin()
  ],
})

Custom Serializer

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

const handler = new OpenAPIHandler(router, {
  serializer: new OpenAPISerializer({
    handlers: {
      // ...custom handlers
    },
  }),
})

Filtering Procedures

Use the filter option to exclude procedures from matching:

const handler = new OpenAPIHandler(router, {
  filter: (contract, path) => getIsInternalMeta(contract) !== true,
})

Custom Error Response

By default, OpenAPIHandler determines response status codes using COMMON_ERROR_STATUS_MAP and encodes error bodies in the ORPC error format. Use errorStatusMap and customErrorResponseBodyEncoder to customize this behavior:

import { COMMON_ERROR_STATUS_MAP } from '@orpc/openapi'

const handler = new OpenAPIHandler(router, {
  /**
   * The status code should be in the `4xx` or `5xx` range (must be greater than or equal to `400`).
   */
  errorStatusMap: {
    ...COMMON_ERROR_STATUS_MAP,
    CUSTOM_ERROR: 599,
  },
  customErrorResponseBodyEncoder: (error) => {
    if (error.code === 'CUSTOM_ERROR') {
      return {
        customMessage: error.message,
        customCode: error.code,
      }
    }

    // fallback to default by returning null or undefined
    return null
  },
})
Common Error Status Map
Error Code HTTP Status Code
BAD_REQUEST 400
UNAUTHORIZED 401
PAYMENT_REQUIRED 402
FORBIDDEN 403
NOT_FOUND 404
METHOD_NOT_SUPPORTED 405
NOT_ACCEPTABLE 406
TIMEOUT 408
CONFLICT 409
GONE 410
PRECONDITION_FAILED 412
PAYLOAD_TOO_LARGE 413
UNSUPPORTED_MEDIA_TYPE 415
UNPROCESSABLE_CONTENT 422
PRECONDITION_REQUIRED 428
TOO_MANY_REQUESTS 429
CLIENT_CLOSED_REQUEST 499
INTERNAL_SERVER_ERROR 500
NOT_IMPLEMENTED 501
BAD_GATEWAY 502
SERVICE_UNAVAILABLE 503
GATEWAY_TIMEOUT 504

Event Stream Options

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

const handler = new OpenAPIHandler(router, {
  toFetchResponse: {
    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?