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/1Declared 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) }),
];