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))
}
unlazyRouterresolves any lazy routers so the whole router can be minified.minifyRouterContractpreserves only the metadata the client needs; schemas and handlers are stripped out.
Create the Link
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}' } } } },
},
// ...
})