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

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_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.

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.