| name | openapi |
| description | Generate OpenAPI 3.1 spec from a tRPC router with @trpc/openapi CLI or programmatic API. Generate typed REST client with @hey-api/openapi-ts and configureTRPCHeyApiClient(). Configure transformers (superjson, EJSON) for generated clients. Alpha status.
|
| type | composition |
| library | trpc |
| library_version | 11.16.0-alpha |
| requires | ["server-setup"] |
| sources | ["trpc/trpc:www/docs/client/openapi.md","trpc/trpc:packages/openapi/test/heyapi.test.ts","trpc/trpc:examples/openapi-codegen/"] |
tRPC -- OpenAPI
Alpha: @trpc/openapi is versioned as 11.x.x-alpha. APIs may change without notice.
Setup
1. Install
pnpm add @trpc/openapi
For HeyAPI client generation:
pnpm add @hey-api/openapi-ts -D
2. Generate the OpenAPI spec
The generator statically analyses your router's TypeScript types. It never executes your code.
CLI:
pnpm exec trpc-openapi ./src/server/index.ts -e appRouter -o openapi.json --title "My API" --version 1.0.0
| Option | Default | Description |
|---|
-e, --export <name> | AppRouter | Name of the exported router |
-o, --output <file> | openapi.json | Output file path |
--title <text> | tRPC API | OpenAPI info.title |
--version <ver> | 0.0.0 | OpenAPI info.version |
Programmatic:
import { generateOpenAPIDocument } from '@trpc/openapi';
const doc = await generateOpenAPIDocument('./src/server/index.ts', {
exportName: 'appRouter',
title: 'My API',
version: '1.0.0',
});
3. Generate a HeyAPI client from the spec
import { rmSync, writeFileSync } from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createClient } from '@hey-api/openapi-ts';
import { generateOpenAPIDocument } from '@trpc/openapi';
import { createTRPCHeyApiTypeResolvers } from '@trpc/openapi/heyapi';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const routerPath = path.resolve(__dirname, '..', 'server', 'index.ts');
const outputDir = path.resolve(__dirname, '..', 'client', 'generated');
const specPath = path.resolve(__dirname, '..', '..', 'openapi.json');
async function main() {
const doc = await generateOpenAPIDocument(routerPath, {
exportName: 'appRouter',
title: 'Example API',
version: '1.0.0',
});
writeFileSync(specPath, JSON.stringify(doc, null, 2) + '\n');
rmSync(outputDir, { recursive: true, force: true });
await createClient({
input: specPath,
output: outputDir,
plugins: [
{
name: '@hey-api/typescript',
'~resolvers': createTRPCHeyApiTypeResolvers(),
},
{
name: '@hey-api/sdk',
operations: { strategy: 'single' },
},
],
});
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Run it:
pnpm tsx scripts/codegen.ts
4. Configure and use the generated client at runtime
import { configureTRPCHeyApiClient } from '@trpc/openapi/heyapi';
import { client } from './generated/client.gen';
import { Sdk } from './generated/sdk.gen';
configureTRPCHeyApiClient(client, {
baseUrl: 'http://localhost:3000',
});
const sdk = new Sdk({ client });
const result = await sdk.greeting({ query: { input: { name: 'World' } } });
const user = await sdk.user.create({ body: { name: 'Bob', age: 30 } });
Core Patterns
CLI quick spec generation
pnpm exec trpc-openapi ./src/server/router.ts
pnpm exec trpc-openapi ./src/server/router.ts -e appRouter -o api.json --title "My API" --version 1.0.0
HeyAPI codegen with type resolvers (transformer setup)
When the server uses a transformer, pass createTRPCHeyApiTypeResolvers() to the @hey-api/typescript plugin so generated types use Date instead of string for date-time fields and bigint for bigint fields:
import { createClient } from '@hey-api/openapi-ts';
import { createTRPCHeyApiTypeResolvers } from '@trpc/openapi/heyapi';
await createClient({
input: './openapi.json',
output: './generated',
plugins: [
{
name: '@hey-api/typescript',
'~resolvers': createTRPCHeyApiTypeResolvers(),
},
{
name: '@hey-api/sdk',
operations: { strategy: 'single' },
},
],
});
Runtime client with superjson transformer
When the tRPC server uses superjson, the client must be configured with the same transformer:
import superjson from 'superjson';
export const transformer = superjson;
import { initTRPC } from '@trpc/server';
import { transformer } from '../shared/transformer';
const t = initTRPC.create({ transformer });
export const router = t.router;
export const publicProcedure = t.procedure;
import { configureTRPCHeyApiClient } from '@trpc/openapi/heyapi';
import superjson from 'superjson';
import { client } from './generated/client.gen';
import { Sdk } from './generated/sdk.gen';
configureTRPCHeyApiClient(client, {
baseUrl: 'http://localhost:3000',
transformer: superjson,
});
const sdk = new Sdk({ client });
const event = await sdk.getEvent({
query: { input: { id: 'evt_1', at: new Date('2025-06-15T10:00:00Z') } },
});
MongoDB EJSON transformer (cross-language)
For non-TypeScript clients, EJSON provides a language-agnostic serialization format:
import type { TRPCDataTransformer } from '@trpc/server';
import type { Document } from 'bson';
import { EJSON } from 'bson';
export const ejsonTransformer: TRPCDataTransformer = {
serialize: (value) => EJSON.serialize(value),
deserialize: (value) => EJSON.deserialize(value as Document),
};
import { configureTRPCHeyApiClient } from '@trpc/openapi/heyapi';
import { client } from './generated/client.gen';
import { ejsonTransformer } from './transformer';
configureTRPCHeyApiClient(client, {
baseUrl: 'http://localhost:3000',
transformer: ejsonTransformer,
});
Response shape
All tRPC HTTP responses follow the envelope format. Access data through result.data:
const listResult = await sdk.user.list();
const users = listResult.data?.result.data;
const createResult = await sdk.user.create({ body: { name: 'nick' } });
const user = createResult.data?.result.data;
Descriptions in the spec
Zod .describe() calls and JSDoc comments on types, routers, and procedures become description fields in the generated OpenAPI spec. No annotations or decorators required.
Common Mistakes
Missing transformer config in HeyAPI client
When the tRPC server uses superjson or another transformer, the generated HeyAPI client must also be configured with the same transformer via configureTRPCHeyApiClient(client, { transformer }). Without this, Date, Map, Set, and other non-JSON types will be silently wrong at runtime -- they arrive as raw serialized objects instead of their native types.
Wrong:
configureTRPCHeyApiClient(client, {
baseUrl: 'http://localhost:3000',
});
Right:
configureTRPCHeyApiClient(client, {
baseUrl: 'http://localhost:3000',
transformer: superjson,
});
Expecting subscriptions in OpenAPI spec
Subscriptions are currently excluded from OpenAPI spec generation. The generator silently skips any procedure with type: 'subscription'. SSE subscription support is planned but not yet available.
Forgetting createTRPCHeyApiTypeResolvers when using a transformer
Without the type resolvers plugin, HeyAPI generates string types for date-time fields instead of Date. The createTRPCHeyApiTypeResolvers() function maps date/date-time format to Date and bigint format to bigint in the generated TypeScript SDK.
Using the wrong export name
The CLI defaults to --export AppRouter (the type). If your file exports the router value as appRouter, pass -e appRouter. If the export is not found, the error message lists all available exports from the file.
See Also
- server-setup -- Required. Define routers and procedures before generating the spec.
- superjson -- Transformer configuration for server and client. OpenAPI clients need matching transformer config.
- validators -- Zod
.describe() calls propagate into OpenAPI description fields.
- Full working example:
examples/openapi-codegen/