Skip to content
tRPC

Calling REST from the client

openApiLink sends each call to its REST route, typed from a contract.

openApiLink sends each call to its REST route. It needs the routes, so it takes a contract; it never needs the router.

import { function toContract<TRouter extends object>(router: TRouter): ContractJson<TRouter>toContract } from '@trpc12/trpc/contract';
import { 
function openApiLink(opts: OpenApiLinkOptions): TrpcLink<{
    form: JsonForm;
}>
openApiLink
} from '@trpc12/trpc/openapi';
const
const contract: ContractJson<{
    post: {
        byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>, "query", {
            output: Post | undefined;
            errors: never;
            services: never;
            unmapped: never;
        }>;
    };
}>
contract
=
toContract<{
    post: {
        byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>, "query", {
            output: Post | undefined;
            errors: never;
            services: never;
            unmapped: never;
        }>;
    };
}>(router: {
    post: {
        byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>, "query", {
            output: Post | undefined;
            errors: never;
            services: never;
            unmapped: never;
        }>;
    };
}): ContractJson<...>
toContract
(
const appRouter: {
    post: {
        byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>, "query", {
            output: Post | undefined;
            errors: never;
            services: never;
            unmapped: never;
        }>;
    };
}
appRouter
); // or the JSON, shipped to the client
const
const client: TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: {};
            meta: {
                route?: Route | undefined;
            };
            input: {
                id: string;
            };
            output: Post | undefined;
            errors: InputValidationError<{
                readonly code: "invalid_type";
                readonly expected: z.core.$ZodInvalidTypeExpected;
                readonly path: readonly [] | readonly ["id"];
                readonly message: string;
            } | {
                readonly code: "too_big";
                readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
                readonly maximum: string | number;
                readonly inclusive?: boolean | undefined;
                readonly exact?: boolean | undefined;
                readonly path: readonly [] | readonly [...];
                readonly message: string;
            } | ... 9 more ... | {
                ...;
            }>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>
client
=
createTrpcClient<ContractJson<{
    post: {
        byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>, "query", {
            output: Post | undefined;
            errors: never;
            services: never;
            unmapped: never;
        }>;
    };
}>, readonly [TrpcLink<{
    form: JsonForm;
}, any>]>(opts: TypedClientOptions<ContractJson<{
    post: {
        byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>, "query", {
            output: Post | undefined;
            errors: never;
            services: never;
            unmapped: never;
        }>;
    };
}>, readonly [...]>): TrpcClient<...>
createTrpcClient
({
TypedClientOptions<ContractJson<{ post: { byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, ZodObject<{ id: ZodString; }, $strip>>, "query", { ...; }>; }; }>, readonly [...]>.router: ContractJson<{
    post: {
        byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>, "query", {
            output: Post | undefined;
            errors: never;
            services: never;
            unmapped: never;
        }>;
    };
}>
router
:
const contract: ContractJson<{
    post: {
        byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>, "query", {
            output: Post | undefined;
            errors: never;
            services: never;
            unmapped: never;
        }>;
    };
}>
contract
,
TypedClientOptions<ContractJson<{ post: { byId: BuildProcedure<InputDef<ExtendedDef<RootDef<Context, {}>, RootDef<{}, OpenApiMeta>>, ZodObject<{ id: ZodString; }, $strip>>, "query", { ...; }>; }; }>, readonly [...]>.links: readonly [TrpcLink<{
    form: JsonForm;
}, any>]
links
: [
function openApiLink(opts: OpenApiLinkOptions): TrpcLink<{
    form: JsonForm;
}>
openApiLink
({
OpenApiLinkOptions.url: string | URLurl: 'https://api.example.com', OpenApiLinkOptions.contract: ContractJson<any>contract,
OpenApiLinkOptions.headers?: HeadersInit | ((opts: {
    op: Operation;
}) => HeadersInit | Promise<HeadersInit>) | undefined
headers
: { 'x-user': 'ada' },
}), ], }); await
const client: TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: {};
            meta: {
                route?: Route | undefined;
            };
            input: {
                id: string;
            };
            output: Post | undefined;
            errors: InputValidationError<{
                readonly code: "invalid_type";
                readonly expected: z.core.$ZodInvalidTypeExpected;
                readonly path: readonly [] | readonly ["id"];
                readonly message: string;
            } | {
                readonly code: "too_big";
                readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
                readonly maximum: string | number;
                readonly inclusive?: boolean | undefined;
                readonly exact?: boolean | undefined;
                readonly path: readonly [] | readonly [...];
                readonly message: string;
            } | ... 9 more ... | {
                ...;
            }>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>
client
.
post: ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: {};
        meta: {
            route?: Route | undefined;
        };
        input: {
            id: string;
        };
        output: Post | undefined;
        errors: InputValidationError<{
            readonly code: "invalid_type";
            readonly expected: z.core.$ZodInvalidTypeExpected;
            readonly path: readonly [] | readonly ["id"];
            readonly message: string;
        } | {
            readonly code: "too_big";
            readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
            readonly maximum: string | number;
            readonly inclusive?: boolean | undefined;
            readonly exact?: boolean | undefined;
            readonly path: readonly [] | readonly [...];
            readonly message: string;
        } | ... 9 more ... | {
            ...;
        }>;
        services: never;
    }>;
}, {}, undefined, LinksTypes<...>>
post
.
byId: {
    query(input: {
        id: string;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<{
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    } | undefined, CallError<{
        type: "query";
        ctx: {};
        meta: {
            route?: Route | undefined;
        };
        input: {
            id: string;
        };
        output: Post | undefined;
        errors: InputValidationError<{
            readonly code: "invalid_type";
            readonly expected: z.core.$ZodInvalidTypeExpected;
            readonly path: readonly [] | readonly ["id"];
            readonly message: string;
        } | {
            readonly code: "too_big";
            readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
            ... 4 more ...;
            readonly message: string;
        } | ... 9 more ... | {
            ...;
        }>;
        services: never;
    }, JsonForm, never>>;
}
byId
.
function query(input: {
    id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
} | undefined, CallError<{
    type: "query";
    ctx: {};
    meta: {
        route?: Route | undefined;
    };
    input: {
        id: string;
    };
    output: Post | undefined;
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        ... 4 more ...;
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    services: never;
}, JsonForm, never>>
query
({ id: stringid: '1' }); // GET https://api.example.com/api/posts/1

Declared errors come back as typed TrpcErrors, as they do over RPC.

REST responses are plain JSON, and the client types say so: a createdAt: Date output is a string through openApiLink. To get the types back, put validationLink({ contract, output: 'json' }) in front: it decodes each result with the Effect Schema's JSON codec, and the client types results as the schemas' types again (Validating on the client):

links: [
  validationLink({ contract: appContract, output: 'json' }),
  openApiLink({ url, contract: toContract(appContract) }),
];