Validation
Define validation schemas for your requests details and response data
Validation Schema allows you to pre-define the URL path and the shape of request and response data. You can easily document your API using this schema. This validation happens at both type-level and runtime.
CallApi uses Standard Schema internally, allowing you to bring your own Standard Schema-compliant validator (e.g., Zod, Valibot, ArkType etc).
Basic Usage
To create a validation schema, you need to import the defineSchema function from @zayne-labs/callapi.
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
export const = ({
"/path": {
: .({
: .(),
: .(),
: .(),
: .(),
}),
: .({
: .(),
: .(),
: .(),
: .(),
}),
},
});
const = ({
: "https://jsonplaceholder.typicode.com",
: ,
});Validation Schema
The Validation Schema is a map of paths/urls and schema. Each path in the schema can define multiple validation rules through the following keys:
Response Validation
data: Validates successful response dataerrorData: Validates error response data
If any validation fails, a ValidationError will be thrown.
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: "https://api.example.com",
: ({
"/products/:id": {
: .({
: .(),
: .(),
: .(),
}),
: .({
: .(),
: .(),
}),
},
}),
});
const { , } = await ("/products/:id", {
: {
: 100,
},
});
errorData for HTTP errors will be typed as { code: string; message: string }Request Validation
body: Validates request body data before sendingheaders: Ensures required headers are present and correctly formattedmethod: Validates or enforces specific HTTP methods (GET, POST, etc.)params: Validates URL parameters (:param)query: Validates query string parameters before they're added to the URL
import { createFetchClient } from "@zayne-labs/callapi";
import { defineSchema } from "@zayne-labs/callapi/utils";
import { z } from "zod";
const callApi = createFetchClient({
baseURL: "https://api.example.com",
schema: defineSchema({
"/users/:userId": {
query: z.object({
id: z.string(),
}),
params: z.object({
userId: z.string(),
}),
},
}),
});Body Validation
The body key is used to validate the request body data, as well provide type safety for the request body data.
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: "https://api.example.com",
: ({
"/products": {
: .({
: .(),
: .(),
: .(),
}),
},
}),
});
const { } = await ("/products", {
body: {},});Headers Validation
The headers key validates request headers. This is useful for enforcing required headers or validating header formats:
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: "https://api.example.com",
: ({
"/products": {
: .({
"x-api-key": .(),
"content-type": .("application/json"),
: .().("Bearer "),
}),
},
}),
});
const { } = await ("/products", {
headers: {},});Meta Validation
The meta key validates the meta option, which can be used to pass arbitrary metadata through the request lifecycle:
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: "https://api.example.com",
: ({
"/analytics": {
: .({
: .(),
: .().(),
}),
},
}),
});
const { } = await ("/analytics", {
meta: {},});Query Parameters
The query schema validates query parameters. If you define a query schema, the parameters will be validated before being added to the URL.
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: "https://api.example.com",
: ({
"/products": {
: .({
: .(),
: .(),
: .(),
}),
},
}),
});
const { } = await ("/products", {
query: {},});Dynamic Path Parameters
The params schema validates URL parameters. You can define dynamic parameters in the following ways:
- Using colon syntax in the schema path (
:paramName) with providing a params schema. This only enforces the type of the parameter at the type level. - Using the
paramsvalidator schema. This takes precedence over the colon syntax.
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: "https://api.example.com",
: ({
// Using colon syntax
"/products/:id": {
: .({
: .(),
: .(),
}),
},
// Using params schema
"/products": {
: .({
: .(),
: .(),
}),
: .({
: .(),
}),
},
// Using both colon syntax and params schema (the params schema takes precedence and ensures validation both at type level and runtime)
"/products/:id/:category": {
: .({
: .(),
: .(),
}),
: .({
: .(),
: .(),
}),
},
}),
});
const = await ("/products/:id", {
: {
: 20,
},
});
const = await ("/products", {
: {
: "v1",
},
});
const = await ("/products/:id/:category", {
: {
: 20,
: "electronics",
},
});HTTP Method Modifiers
You can specify the HTTP method in two ways:
- Using the
methodvalidator schema - Prefixing the path with
@method-name
The supported method modifiers are:
@get/@post/@put/@patch/@delete/
If you use the @method-name/ prefix, it will added to the request options automatically. You can override it by explicitly passing the method option to the callApi function.
import { createFetchClient } from "@zayne-labs/callapi";
import { defineSchema } from "@zayne-labs/callapi/utils";
import { z } from "zod";
const callApi = createFetchClient({
baseURL: "https://api.example.com",
schema: defineSchema({
// Using method prefix
"@post/products": {
body: z.object({
title: z.string(),
price: z.number(),
}),
},
// Using method validator
"products/:id": {
method: z.literal("DELETE"),
},
}),
});Validation Schema Per Instance
You can also define a validation schema for a specific instance of callApi instead of globally on createFetchClient:
import { createFetchClient } from "@zayne-labs/callapi";
import { z } from "zod";
const callApi = createFetchClient({
baseURL: "https://api.example.com",
});
const { data, error } = await callApi("/user", {
schema: {
data: z.object({
userId: z.string(),
id: z.number(),
title: z.string(),
completed: z.boolean(),
}),
},
});Custom Validators
Instead of using Zod schemas, you can also provide custom validator functions for any schema field.
These functions receive the input value and can perform custom validation or transformation. They can also be async if need be.
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
// Simulation: Get allowed domains from config/API
const = (: number) => new (() => (, ));
const = async () => {
await (1000);
return ["example.com", "company.com"];
};
const = ({
: "https://api.example.com",
: ({
"/users": {
// Async body validator with custom validation
: async () => {
if (! || typeof !== "object") {
throw new ("Invalid request body");
}
// Required fields
if (!("name" in ) || typeof . !== "string") {
throw new ("Name is required");
}
if (!("email" in ) || typeof . !== "string" || !..("@")) {
throw new ("Valid email required");
}
// Validate domain against allowed list
const = ..("@")[1] ?? "";
const = await ();
if (!.()) {
throw new (`Email domain ${} not allowed`);
}
return {
: ..(),
: ..(),
};
},
// Response data validator
: () => {
if (
!
|| typeof !== "object"
|| !("id" in )
|| !("name" in )
|| !("email" in )
) {
throw new ("Invalid response data");
}
return ; // Type will be narrowed to { id: number; name: string; email: string }
},
},
}),
});
const { } = await ("/users", {Types are inferred from validator return types : {
: "JOHN@example.com",
: " John ", // Will be trimmed & lowercased.
},
});Custom validators allow you to:
- Accept raw input data to validate
- Run sync or async validation logic
- Transform data if needed (e.g., normalize, sanitize)
- Can throw errors for invalid data
- Return the validated data (From which TypeScript infers the return type)
Overriding the base schema for a specific path
You can override the base schema by passing a schema to the schema option:
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: "https://api.example.com",
: ({
"/user": {
: .({
: .(),
: .(),
}),
},
}),
});
const { , } = await ("/user", {
// @annotate: This will override the base schema for this specific path
: {
: .({
: .(),
: .(),
: .(),
}),
},
});Extending the base schema for a specific path
In case you want to extend the base schema instead of overriding it, you can pass a callback to schema option of the instance, which will be called with the base schema and the specific schema for current path:
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: "https://api.example.com",
: ({
"/user": {
: .({
: .(),
: .(),
: .(),
: .(),
}),
},
}),
});
const { , } = await ("/user", {
// @annotate: This will extend the base schema for this specific path
: ({ }) => ({
...,
: .({
: .(),
: .(),
}),
}),
});Fallback Route Schema
You can define a fallback schema that applies to all routes not explicitly defined in your schema using the special @default key (or use the exported fallBackRouteSchemaKey constant):
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: ({
// Fallback schema for all routes
"@default": {
: .({
"x-api-key": .(),
}),
},
// Specific route schema (takes precedence over fallback)
"/users": {
: .({
: .(),
: .(),
}),
},
}),
});
// This will use the fallback schema (requires x-api-key header)
await ("/posts");
// This will use both the fallback and specific schema
await ("/users");Alternatively, use the exported constant for better maintainability:
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/constants";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: ({
[]: {
: .({
"x-api-key": .(),
}),
},
"/users": {
: .({
: .(),
: .(),
}),
},
}),
});Strict Mode
By default, CallAPI allows requests to paths not defined in the schema. Enable strict mode to prevent usage of undefined paths:
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = ({
: (
{
"/user": {
: .({
: .(),
: .(),
: .(),
: .(),
}),
},
},
{
: true,
}
),
});
const { , } = await ("/invalid-path");Validation Error Handling
CallAPI throws a ValidationError when validation fails. The error includes detailed information about what failed:
import { isValidationError } from "@zayne-labs/callapi/utils";
const { data, error } = await callApi("/products/:id", {
params: { id: "invalid" }, // Should be a number
});
if (isValidationError(error)) {
console.log(error.issues); // Validation issues
}Types
CallApiSchema
Prop
Type
CallApiSchemaConfig
Prop
Type
Last updated on