OpenAPI
Serve the router over REST, document it as OpenAPI 3.1, call REST routes from the client, and generate contracts from OpenAPI documents.
@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 createOpenApiHandler<TRouter extends AnyRouter, TPlatform = undefined>(opts: OpenApiHandlerOptions<TRouter, TPlatform>): OpenApiHandler<TPlatform>createOpenApiHandler,
function generateOpenApi(router: object, opts: GenerateOpenApiOptions): OpenApiDocumentgenerateOpenApi,
function openapi(): ProcedureBuilder<RootDef<{}, OpenApiMeta>>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 openapi(): ProcedureBuilder<RootDef<{}, OpenApiMeta>>openapi());
const const router: {
post: {
byId: BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | null;
errors: never;
services: never;
unmapped: never;
}>;
};
}
router = {
post: {
byId: BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | null;
errors: never;
services: never;
unmapped: never;
}>;
}
post: {
byId: BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | null;
errors: never;
services: never;
unmapped: 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): BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | null;
errors: never;
services: never;
unmapped: 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 = createOpenApiHandler<{
post: {
byId: BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | null;
errors: never;
services: never;
unmapped: never;
}>;
};
}, undefined>(opts: OpenApiHandlerOptions<{
post: {
byId: BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | null;
errors: never;
services: never;
unmapped: never;
}>;
};
}, undefined>): OpenApiHandler<...>
createOpenApiHandler({ BaseOpenApiHandlerOptions<{ post: { byId: BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, ZodObject<{ id: ZodString; }, $strip>>, "query", { ...; }>; }; }>.router: {
post: {
byId: BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | null;
errors: never;
services: never;
unmapped: never;
}>;
};
}
router });
const const doc: OpenApiDocumentdoc = function generateOpenApi(router: object, opts: GenerateOpenApiOptions): OpenApiDocumentgenerateOpenApi(const router: {
post: {
byId: BuildProcedure<InputDef<ExtendedDef<RootDef<{}, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | null;
errors: never;
services: never;
unmapped: 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:
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.