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: aTrpcError.Taggedclass's literalstatus, the built-in code's status, or400; - 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:
Dateis adate-timestring, andtoJSON()is followed.- Functions, symbols,
bigintandundefinedare left out; optional properties aren'trequired. - 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.