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.
Alpha:
@trpc/openapiis versioned as11.x.x-alpha. APIs may change without notice.
pnpm add @trpc/openapi
For HeyAPI client generation:
pnpm add @hey-api/openapi-ts -D
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',
});
// scripts/codegen.ts
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
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 });
// Queries -> GET, Mutations -> POST
const result = await sdk.greeting({ query: { input: { name: 'World' } } });
const user = await sdk.user.create({ body: { name: 'Bob', age: 30 } });
# Default export name "AppRouter", output "openapi.json"
pnpm exec trpc-openapi ./src/server/router.ts
# Custom export name and output
pnpm exec trpc-openapi ./src/server/router.ts -e appRouter -o api.json --title "My API" --version 1.0.0
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' },
},
],
});
When the tRPC server uses superjson, the client must be configured with the same transformer:
// src/shared/transformer.ts
import superjson from 'superjson';
export const transformer = superjson;
// src/server/trpc.ts
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;
// src/client/index.ts
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') } },
});
// event.data.result.data.at is a Date object
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,
});
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; // the actual return value
const createResult = await sdk.user.create({ body: { name: 'nick' } });
const user = createResult.data?.result.data;
// user.createdAt instanceof Date === true (when transformer is configured)
Zod .describe() calls and JSDoc comments on types, routers, and procedures become description fields in the generated OpenAPI spec. No annotations or decorators required.
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',
// missing transformer -- Dates will be broken
});
Right:
configureTRPCHeyApiClient(client, {
baseUrl: 'http://localhost:3000',
transformer: superjson, // must match server's transformer
});
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.
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.
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.
.describe() calls propagate into OpenAPI description fields.examples/openapi-codegen/