Skip to content

tRPC v12 isn’t on npm yet. The @trpc12/* packages on this site can’t be installed until it’s released. Use v11 today

tRPC

ExperimentalContracts and OpenAPI might change or go away in any release. Stability

The document

Generate an OpenAPI 3.1 document from the router's schemas, or from its types.

On this page
import { function experimental_generateOpenApi(router: object, opts: GenerateOpenApiOptions): OpenApiDocumentexperimental_generateOpenApi } from '@trpc12/trpc/openapi';
import { function loadRouter<TRouter extends AnyRouter>(router: TRouter): Promise<TRouter>loadRouter } from '@trpc12/trpc/server';

const const doc: OpenApiDocumentdoc = function experimental_generateOpenApi(router: object, opts: GenerateOpenApiOptions): OpenApiDocumentexperimental_generateOpenApi(await 
loadRouter<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}>(router: {
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}): Promise<...>
loadRouter
(
const appRouter: {
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}
appRouter
), {
GenerateOpenApiOptions.info: OpenApiInfoinfo: { OpenApiInfo.title: stringtitle: 'API', OpenApiInfo.version: stringversion: '1.0.0' }, GenerateOpenApiOptions.servers?: readonly OpenApiServer[] | undefinedservers: [{ OpenApiServer.url: stringurl: 'https://api.example.com' }], });

The document has schemas for input, output and each declared error. filter({ path, procedure }) drops operations. target: 'rpc' describes the RPC endpoint instead of the REST routes. experimental_generateOpenApi throws on a lazy subtree that hasn't loaded, so pass the result of loadRouter().

From the types

experimental_generateOpenApi only sees runtime schemas. experimental_generateOpenApiFromTypes from @trpc12/trpc/openapi/typescript also reads the router's types with the TypeScript 7 compiler, so it documents:

  • errors that are only inferred (returned with error(), failed or yielded in an Effect), never declared, with their status: a TrpcError.Tagged class's literal status, the built-in code's status, or 400;
  • the input and output of procedures without static schemas (no .output(), or a ctx-callback input).
// scripts/openapi.ts, run at build time
import { function experimental_generateOpenApiFromTypes(router: object, opts: GenerateOpenApiFromTypesOptions): Promise<OpenApiDocument>experimental_generateOpenApiFromTypes } from '@trpc12/trpc/openapi/typescript';
import { 
const appRouter: {
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}
appRouter
} from '../src/router.ts';
const const doc: OpenApiDocumentdoc = await function experimental_generateOpenApiFromTypes(router: object, opts: GenerateOpenApiFromTypesOptions): Promise<OpenApiDocument>experimental_generateOpenApiFromTypes(
const appRouter: {
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}
appRouter
, {
GenerateOpenApiFromTypesOptions.file: string | URLfile: new var URL: new (url: string | URL, base?: string | URL) => URLURL('../src/router.ts', import.meta.ImportMeta.url: stringurl), GenerateOpenApiFromTypesOptions.exportName?: string | undefinedexportName: 'appRouter', // the default GenerateOpenApiOptions.info: OpenApiInfoinfo: { OpenApiInfo.title: stringtitle: 'API', OpenApiInfo.version: stringversion: '1.0.0' }, }); await module "node:fs/promises"fs.
function writeFile(file: PathLike | fs.FileHandle, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: (ObjectEncodingOptions & {
    mode?: Mode | undefined;
    flag?: OpenMode | undefined;
    flush?: boolean | undefined;
} & EventEmitter<T extends EventMap<T> = any>.Abortable) | BufferEncoding | null): Promise<void>
writeFile
('openapi.json', var JSON: JSONJSON.JSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)stringify(const doc: OpenApiDocumentdoc, null, 2));
// server import import docdoc from './openapi.json' with { type: 'json' }; function experimental_openApiReference(opts: OpenApiReferenceOptions): HttpMiddlewareexperimental_openApiReference({ document: OpenApiDocument | (() => MaybePromise<OpenApiDocument>)document: import docdoc as OpenApiDocument });

Where a procedure has a runtime schema, that schema wins, so the two generators agree. Types become JSON Schema the way JSON.stringify sends them:

  • Date is a date-time string, and toJSON() is followed.
  • Functions, symbols, bigint and undefined are left out; optional properties aren't required.
  • Branded primitives (string & { __brand }) are their primitive.
  • Recursive types become components.schemas.
  • JSDoc comments become descriptions.

typescript (7.x) is an optional peer dependency, needed only where this runs. tsconfig picks the project; it defaults to the nearest one that includes file.

An output declared with .output(schema, { validate: false }) isn't validated. The document then describes the schema's input side, which is what the resolver returns.

Credits

The generator follows oRPC's OpenAPI specification, and experimental_openApiReference() its OpenAPI reference plugin.