Skip to content
You are reading the v2 docs, currently in beta.V1 docs
oRPC
Esc
navigateopen⌘Jpreview
On this page

OpenAPI Link Without Runtime Imports

Use build-time macros to embed a minified contract in your client bundle, so OpenAPI Link works without maintaining a separate contract or shipping server code to the client.

OpenAPI Link needs a contract at runtime to know each procedure’s method and path. With RPC Link a type import is enough, but with OpenAPI Link you must either maintain a contract or import your router, which pulls server code into the client bundle. Safely Importing Router on the Client avoids that by exporting a minified contract to a JSON file, but you must regenerate the file every time the router changes.

Macros remove that manual step. A macro is a function your bundler runs at build time, replacing the call with its return value. Bun supports macros natively, and unplugin-macros brings the same syntax to Vite, Rollup, webpack, esbuild, and Rspack. With a macro that returns the minified contract, every build embeds an up-to-date contract in the client bundle, and server code never leaves the server.

Setup

If you bundle with Bun, macros work out of the box. For other bundlers, install unplugin-macros and register it. For example, with Vite:

import Macros from 'unplugin-macros/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [Macros()],
})

Export the Contract from a Macro

Define a function that derives the minified contract from your router:

import { minifyRouterContract, RouterContract } from '@orpc/contract'
import { unlazyRouter } from '@orpc/server'
import { router } from './router'

export async function getMinifiedContract(): Promise<RouterContract> {
  return minifyRouterContract(await unlazyRouter(router))
}
  • unlazyRouter resolves any lazy routers so the whole router can be minified.
  • minifyRouterContract preserves only the metadata the client needs; schemas and handlers are stripped out.

Import the function with the { type: 'macro' } attribute and pass its result to OpenAPILink:

import type { JsonifiedClient } from '@orpc/openapi'
import type { RouterClient } from '@orpc/server'
import type { router } from './router'
import { createORPCClient } from '@orpc/client'
import { OpenAPILink } from '@orpc/openapi/fetch'
import { getMinifiedContract } from './contract.ts' with { type: 'macro' } 

const link = new OpenAPILink(await getMinifiedContract(), {
  origin: 'https://api.example.com',
  url: '/api',
})

const client: JsonifiedClient<RouterClient<typeof router>> = createORPCClient(link)

The router import is type-only, so it is erased at compile time and never reaches the bundle. The bundler calls getMinifiedContract at build time and inlines the result, so the bundle contains only plain JSON data:

const link = new OpenAPILink({
  planet: {
    find: { '~orpc': { errorMap: {}, meta: { '~openapi': { method: 'GET', path: '/planets/{id}' } } } },
  },
  // ...
})

Last updated on August 24, 2026

Was this page helpful?