How to generate OpenAPI schema in Next.js build process?

HEAD
ff2168fchore: update dependencies and pm (#1380)
This post might have stale content, as HEAD is 11 commits ahead.

how do i setup my next.js app so that it generates an openapi schema as part of the build/deploy process? i'm deploying to vercel btw

Avatar of anthropic/claude-opus-4.5
Claude Opus 4.5
commented

I now have all the information needed to provide a comprehensive answer. Based on my research, there are two main approaches for setting up OpenAPI schema generation with oRPC in a Next.js app:


##Two Approaches to OpenAPI Schema Generation

This is the simplest approach and works great with Vercel. The OpenAPI spec is generated on-demand when someone requests it.

Setup in src/app/api/[[...rest]]/route.ts:

import { router } from '@/routers'
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4' // or '@orpc/zod' for Zod v3

const openAPIHandler = new OpenAPIHandler(router, {
  plugins: [
    new OpenAPIReferencePlugin({
      schemaConverters: [
        new ZodToJsonSchemaConverter(),
      ],
      specGenerateOptions: {
        info: {
          title: 'My API',
          version: '1.0.0',
        },
        // Optional: servers is auto-inferred from request if not specified
        servers: [
          { url: 'https://your-app.vercel.app/api' },
        ],
      },
    }),
  ],
})

async function handleRequest(request: Request) {
  const { response } = await openAPIHandler.handle(request, {
    prefix: '/api',
    context: {},
  })

  return response ?? new Response('Not found', { status: 404 })
}

export const GET = handleRequest
export const POST = handleRequest
export const PUT = handleRequest
export const PATCH = handleRequest
export const DELETE = handleRequest

Result:

  • Docs UI: https://your-app.vercel.app/api/ (Scalar/Swagger)
  • OpenAPI JSON: https://your-app.vercel.app/api/spec.json

This generates the spec at runtime - every request to /api/spec.json generates the spec on the fly. For most use cases, this is perfectly fine and even has the benefit of always being up-to-date.


###2. Build-Time Generation (Static File)

If you need to generate the spec as a static file during your build/deploy process (e.g., for CI pipelines, external tooling, or caching), you can create a script:

Create scripts/generate-openapi.ts:

import { writeFileSync } from 'node:fs'
import { OpenAPIGenerator } from '@orpc/openapi'
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
import { router } from '../src/routers' // adjust path to your router

const generator = new OpenAPIGenerator({
  schemaConverters: [
    new ZodToJsonSchemaConverter(),
  ],
})

async function main() {
  const spec = await generator.generate(router, {
    info: {
      title: 'My API',
      version: '1.0.0',
    },
    servers: [
      { url: 'https://your-app.vercel.app/api' },
    ],
  })

  writeFileSync('./public/openapi.json', JSON.stringify(spec, null, 2))
  console.log('✅ OpenAPI spec generated at public/openapi.json')
}

main()

Add to package.json:

{
  "scripts": {
    "generate:openapi": "tsx scripts/generate-openapi.ts",
    "build": "npm run generate:openapi && next build"
  }
}

Vercel Configuration:

Since Vercel runs npm run build (or your specified build command) during deployment, the OpenAPI spec will be generated as part of the build process and placed in public/openapi.json. It will then be served as a static file.

Alternatively, you can configure the build command in vercel.json:

{
  "buildCommand": "npm run generate:openapi && next build"
}

##Which Approach to Choose?

ApproachProsCons
Runtime (Plugin)Always up-to-date, no build step needed, simpler setupSlight overhead on first request (cached after)
Build-Time (Script)Static file, can be used in CI/CD pipelines, no runtime overheadMust rebuild to update, more setup required

For Vercel deployments, the runtime approach with OpenAPIReferencePlugin is typically the best choice since:

  1. It's simpler to set up
  2. The spec auto-infers the server URL from the request
  3. Vercel's edge caching handles repeated requests efficiently

The build-time approach is better if you need to:

  • Feed the spec to external tools during CI
  • Generate client SDKs during the build
  • Ensure the spec file exists before deployment completes
Avatar of julianbenegas
Julian Benegas
commented

how do i use it in the client after that? in say option 1?

Avatar of anthropic/claude-opus-4.5
Claude Opus 4.5
commented

Now I have a complete picture. Here's how to use the OpenAPI client:


With Option 1 (runtime generation via OpenAPIReferencePlugin), you don't actually need the generated spec.json for your oRPC client - the client uses the contract/router types directly for type safety. The spec.json is mainly for documentation (Swagger/Scalar UI) or for external tools.

###Client Setup

Install the client package:

npm install @orpc/openapi-client@latest @orpc/client@latest

Create src/lib/orpc.ts:

import type { JsonifiedClient } from '@orpc/openapi-client'
import type { ContractRouterClient } from '@orpc/contract'
import { createORPCClient } from '@orpc/client'
import { OpenAPILink } from '@orpc/openapi-client/fetch'
import { router } from '@/routers' // your router (acts as contract)

// Create the link pointing to your API
const link = new OpenAPILink(router, {
  url: typeof window !== 'undefined' 
    ? `${window.location.origin}/api` 
    : 'http://localhost:3000/api',
  headers: () => ({
    Authorization: `Bearer ${getToken()}`, // if you need auth
  }),
})

// Create the typed client
// JsonifiedClient wraps types to account for JSON serialization (e.g., Date -> string)
export const client: JsonifiedClient<ContractRouterClient<typeof router>> = createORPCClient(link)

Use it in your components:

// In a React component or server component
const planets = await client.planet.list({ limit: 10 })
//    ^? JsonifiedClient transforms Date fields to strings, etc.

###With TanStack Query (React Query)

If you're using React Query:

// src/lib/orpc.ts
import type { JsonifiedClient } from '@orpc/openapi-client'
import type { ContractRouterClient } from '@orpc/contract'
import { createORPCClient } from '@orpc/client'
import { OpenAPILink } from '@orpc/openapi-client/fetch'
import { createRouterUtils } from '@orpc/tanstack-query'
import { router } from '@/routers'

const link = new OpenAPILink(router, {
  url: typeof window !== 'undefined' 
    ? `${window.location.origin}/api` 
    : 'http://localhost:3000/api',
})

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

// Create TanStack Query utilities
export const orpc = createRouterUtils(client)

In your component:

'use client'
import { orpc } from '@/lib/orpc'

export function PlanetList() {
  const { data, isLoading } = orpc.planet.list.useQuery({ 
    input: { limit: 10 } 
  })

  if (isLoading) return <div>Loading...</div>
  
  return (
    <ul>
      {data?.map(planet => (
        <li key={planet.id}>{planet.name}</li>
      ))}
    </ul>
  )
}

##Key Points

AspectDetails
Type SourceTypes come from your router/contract, not the spec.json
JsonifiedClientWraps types to reflect JSON serialization (e.g., Datestring)
spec.json PurposeFor Swagger/Scalar UI docs, or external tools (not needed for oRPC client)
URLPoints to your API prefix (e.g., /api)

If you don't need OpenAPI compliance on the wire (just the spec for docs), you can use RPCLink instead, which is simpler and supports more data types natively:

import { RPCLink } from '@orpc/client/fetch'

const link = new RPCLink({
  url: `${window.location.origin}/rpc`,
})

But this uses oRPC's custom RPC protocol, not RESTful OpenAPI routes. Use OpenAPILink if you want your client calls to hit proper REST endpoints like GET /api/planets instead of POST /rpc.


END OF POST

How to generate OpenAPI schema in Next.js build process? — middleapi/orpc