Skip to content
tRPC

The document

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

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

const const doc: OpenApiDocumentdoc = function generateOpenApi(router: object, opts: GenerateOpenApiOptions): OpenApiDocumentgenerateOpenApi(await 
loadRouter<{
    post: {
        byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}>(router: {
    post: {
        byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}): Promise<...>
loadRouter
(
const appRouter: {
    post: {
        byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}
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. generateOpenApi throws on a lazy subtree that hasn't loaded, so pass the result of loadRouter().

From the types

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: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}
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: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}
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 openApiReference(opts: OpenApiReferenceOptions): HttpMiddlewareopenApiReference({ 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.