ExperimentalContracts and OpenAPI might change or go away in any release. Stability
OpenAPI
Serve the router over REST, document it as OpenAPI 3.1, call REST routes from the client, and generate contracts from OpenAPI documents.
On this page
@trpc12/trpc/openapi puts the same router behind REST routes and an OpenAPI 3.1 document, so tools and clients that don't speak tRPC can call it. RPC and REST run side by side; neither changes the other.
import {
function experimental_createOpenApiHandler<TRouter extends AnyRouter, TPlatform = undefined>(opts: OpenApiHandlerOptions<TRouter, TPlatform>): OpenApiHandler<TPlatform>experimental_createOpenApiHandler,
function experimental_generateOpenApi(router: object, opts: GenerateOpenApiOptions): OpenApiDocumentexperimental_generateOpenApi,
function experimental_openapi(): ProcedureBuilder<RootDef<{}, OpenApiMeta>>experimental_openapi,
} from '@trpc12/trpc/openapi';
import { function initTrpc<TConfig extends RootConfig = {}>(opts?: RootOptions<TConfig["meta"] extends object ? TConfig["meta"] : {}>): TrpcRoot<TConfig["ctx"] extends object ? TConfig["ctx"] : {}, TConfig["meta"] extends object ? TConfig["meta"] : {}>initTrpc } from '@trpc12/trpc/server';
import { import zz } from 'zod';
const const t: TrpcRoot<{}, {
route?: Route | undefined;
}, ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>>
t = initTrpc<{}>(opts?: RootOptions<{}>): TrpcRoot<{}, {}, RootDef<{}, {}>>initTrpc().TrpcRoot<{}, {}, RootDef<{}, {}>>.concat<RootDef<{}, OpenApiMeta>>(extension: ProcedureBuilder<RootDef<{}, OpenApiMeta>>): TrpcRoot<{}, {
route?: Route | undefined;
}, ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>>
concat(function experimental_openapi(): ProcedureBuilder<RootDef<{}, OpenApiMeta>>experimental_openapi());
const const router: {
post: {
byId: Procedure<{
type: "query";
ctx: {};
meta: {
route?: Route | undefined;
};
input: {
id: string;
};
output: Post | null;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>;
};
}
router = {
post: {
byId: Procedure<{
type: "query";
ctx: {};
meta: {
route?: Route | undefined;
};
input: {
id: string;
};
output: Post | null;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>;
}
post: {
byId: Procedure<{
type: "query";
ctx: {};
meta: {
route?: Route | undefined;
};
input: {
id: string;
};
output: Post | null;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>
byId: const t: TrpcRoot<{}, {
route?: Route | undefined;
}, ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>>
t.TrpcRoot<{}, { route?: Route | undefined; }, ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>>.procedure: ProcedureBuilder<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>>procedure
.ProcedureBuilder<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>>.meta<{
readonly route: {
readonly method: "GET";
readonly path: "/posts/{id}";
};
}>(meta: {
readonly route: {
readonly method: "GET";
readonly path: "/posts/{id}";
};
} & UndeclaredMeta<{
route?: Route | undefined;
}, {
readonly route: {
readonly method: "GET";
readonly path: "/posts/{id}";
};
}>): ProcedureBuilder<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>>
meta({ route: {
readonly method: "GET";
readonly path: "/posts/{id}";
}
route: { method: "GET"method: 'GET', path: "/posts/{id}"path: '/posts/{id}' } })
.ProcedureBuilder<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>>.input<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>> (+1 overload)
input(import zz.function object<{
id: z.ZodString;
}>(shape?: {
id: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>
object({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.ProcedureBuilder<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, ZodObject<{ id: ZodString; }, $strip>>>.query<Post | null>(resolver: (opts: ResolverOpts<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>) => Post | null): Procedure<{
type: "query";
ctx: {};
meta: {
route?: Route | undefined;
};
input: {
id: string;
};
output: Post | null;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>
query((opts: ResolverOpts<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>
opts) => const db: {
post: {
find: (id: string) => Post | undefined;
list: () => Post[];
create: (input: {
title: string;
author?: string | null;
}) => Post;
};
}
db.post: {
find: (id: string) => Post | undefined;
list: () => Post[];
create: (input: {
title: string;
author?: string | null;
}) => Post;
}
post.find: (id: string) => Post | undefinedfind(opts: ResolverOpts<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>
opts.ResolverOpts<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, ZodObject<{ id: ZodString; }, $strip>>>.input: {
id: string;
}
input.id: stringid) ?? null),
},
};
const const rest: OpenApiHandler<undefined>rest = experimental_createOpenApiHandler<{
post: {
byId: Procedure<{
type: "query";
ctx: {};
meta: {
route?: Route | undefined;
};
input: {
id: string;
};
output: Post | null;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>;
};
}, undefined>(opts: OpenApiHandlerOptions<{
post: {
byId: Procedure<{
type: "query";
ctx: {};
meta: {
route?: Route | undefined;
};
input: {
id: string;
};
output: Post | null;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>;
};
}, undefined>): OpenApiHandler<...>
experimental_createOpenApiHandler({ BaseOpenApiHandlerOptions<{ post: { byId: Procedure<{ type: "query"; ctx: {}; meta: { route?: Route | undefined; }; input: { id: string; }; output: Post | null; errors: SchemaValidationError<ZodObject<{ id: ZodString; }, $strip>>; services: never; }>; }; }>.router: {
post: {
byId: Procedure<{
type: "query";
ctx: {};
meta: {
route?: Route | undefined;
};
input: {
id: string;
};
output: Post | null;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>;
};
}
router });
const const doc: OpenApiDocumentdoc = function experimental_generateOpenApi(router: object, opts: GenerateOpenApiOptions): OpenApiDocumentexperimental_generateOpenApi(const router: {
post: {
byId: Procedure<{
type: "query";
ctx: {};
meta: {
route?: Route | undefined;
};
input: {
id: string;
};
output: Post | null;
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: never;
}>;
};
}
router, {
GenerateOpenApiOptions.info: OpenApiInfoinfo: { OpenApiInfo.title: stringtitle: 'Blog', OpenApiInfo.version: stringversion: '1.0.0' },
});GET /api/posts/1 now calls post.byId, and doc describes it with the input and output schemas.
- REST routes: route
meta, the handler, how inputs map to paths, queries and bodies, and outputs and errors. - The document: generate the document from the router's schemas, or from its types.
- Calling REST from the client:
experimental_openApiLinksends each call to its REST route, typed from a contract. - A contract from OpenAPI: generate a typed contract module from any OpenAPI 3.0 or 3.1 document, to call APIs that aren't tRPC.
The examples/experimental/openapi app serves one router over RPC and REST, generates its document from types, and calls it with fetch and experimental_openApiLink.
Credits
REST routes declared in procedure meta come from trpc-openapi, by James Berry, which first brought OpenAPI to tRPC routers; trpc-to-openapi maintains it for v11. Serving the whole router over REST with no configuration, .route({ spec }) and OpenAPI-to-contract follow oRPC's OpenAPI support.