Validating on the client
validationLink checks inputs before sending and results when they arrive, against a contract.
Clients ship no schemas by default. To check calls against a contract in the browser, add validationLink: inputs are validated before anything is sent (the same BAD_REQUEST the server returns, so forms can show its issues), and results and subscription events are checked when they arrive (PARSE_ERROR when the server drifted from the contract):
const const client: TrpcClient<{
post: {
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractRootDef<{}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
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 ["id"];
readonly message: string;
} | ... 9 more ... | {
...;
}>;
requires: never;
}>, {
...;
}>>;
};
}, {}, undefined, LinksTypes<...>>
client = createTrpcClient<{
post: {
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractRootDef<{}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
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 ["id"];
readonly message: string;
} | ... 9 more ... | {
...;
}>;
requires: never;
}>, {
...;
}>>;
};
}, readonly [...]>(opts: TypedClientOptions<...>): TrpcClient<...>
createTrpcClient({
TypedClientOptions<{ post: { byId: ContractProcedure<"query", ContractWith<ContractWith<ContractRootDef<{}>, { inputIn: { id: string; }; inputOut: { id: string; }; errors: InputValidationError<{ readonly code: "invalid_type"; readonly expected: $ZodInvalidTypeExpected; readonly path: readonly [] | readonly [...]; readonly message: string; } | ... 10 more ... | { ...; }>; requires: never; }>, { ...; }>>; }; }, readonly [...]>.router: {
post: {
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractRootDef<{}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
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 ["id"];
readonly message: string;
} | ... 9 more ... | {
...;
}>;
requires: never;
}>, {
...;
}>>;
};
}
router: const appContract: {
post: {
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractRootDef<{}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
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 ["id"];
readonly message: string;
} | ... 9 more ... | {
...;
}>;
requires: never;
}>, {
...;
}>>;
};
}
appContract,
TypedClientOptions<{ post: { byId: ContractProcedure<"query", ContractWith<ContractWith<ContractRootDef<{}>, { inputIn: { id: string; }; inputOut: { id: string; }; errors: InputValidationError<{ readonly code: "invalid_type"; readonly expected: $ZodInvalidTypeExpected; readonly path: readonly [] | readonly [...]; readonly message: string; } | ... 10 more ... | { ...; }>; requires: never; }>, { ...; }>>; }; }, readonly [...]>.links: readonly [TrpcLink<{}, any>, TrpcLink<{
form: JsonForm;
provides: "response";
}, any>]
links: [function validationLink(opts: ValidationLinkOptions): TrpcLink (+1 overload)validationLink({ ValidationLinkOptions.contract: objectcontract: const appContract: {
post: {
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractRootDef<{}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
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 ["id"];
readonly message: string;
} | ... 9 more ... | {
...;
}>;
requires: never;
}>, {
...;
}>>;
};
}
appContract }), httpLink<{
url: string;
}>(opts: {
url: string;
}): TrpcLink<{
form: JsonForm;
provides: "response";
}, any>
httpLink({ url: stringurl })],
});Results are checked against the output's type side, so Effect Schema transformations don't run a second time. With openApiLink, whose results are plain JSON, output: 'json' decodes them with the Effect Schema's JSON codec, so Dates, bigints and classes come back as their types. Standard Schema outputs are validated as they are.
See Contract-first for writing the contract, and Calling REST from the client for openApiLink.