Back to Trpc

tRPC -- OpenAPI

packages/openapi/skills/openapi/SKILL.md

11.16.08.2 KB
Original Source

tRPC -- OpenAPI

Alpha: @trpc/openapi is versioned as 11.x.x-alpha. APIs may change without notice.

Setup

1. Install

bash
pnpm add @trpc/openapi

For HeyAPI client generation:

bash
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:

bash
pnpm exec trpc-openapi ./src/server/index.ts -e appRouter -o openapi.json --title "My API" --version 1.0.0
OptionDefaultDescription
-e, --export <name>AppRouterName of the exported router
-o, --output <file>openapi.jsonOutput file path
--title <text>tRPC APIOpenAPI info.title
--version <ver>0.0.0OpenAPI info.version

Programmatic:

ts
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

ts
// 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:

bash
pnpm tsx scripts/codegen.ts

4. Configure and use the generated client at runtime

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 } });

Core Patterns

CLI quick spec generation

bash
# 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

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:

ts
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:

ts
// src/shared/transformer.ts
import superjson from 'superjson';

export const transformer = superjson;
ts
// 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;
ts
// 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

MongoDB EJSON transformer (cross-language)

For non-TypeScript clients, EJSON provides a language-agnostic serialization format:

ts
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),
};
ts
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:

ts
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)

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:

ts
configureTRPCHeyApiClient(client, {
  baseUrl: 'http://localhost:3000',
  // missing transformer -- Dates will be broken
});

Right:

ts
configureTRPCHeyApiClient(client, {
  baseUrl: 'http://localhost:3000',
  transformer: superjson, // must match server's transformer
});

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/