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

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: openApiLink sends 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.