Skip to content
tRPC

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.