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.

api.ts
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 data
  • errorData: Validates error response data

If any validation fails, a ValidationError will be thrown.

client.ts
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 sending
  • headers: Ensures required headers are present and correctly formatted
  • method: 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
client.ts
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.

client.ts
import {  } from "@zayne-labs/callapi";
import {  } from "@zayne-labs/callapi/utils";
import {  } from "zod";

const  = ({
	: "https://api.example.com",
	: ({
		"/products": {
			: .({
				: .(),
				: .(),
				: .(),
			}),
		},
	}),
});

const {  } = await ("/products", {
	body: {},
Type '{}' is missing the following properties from type '{ title: string; price: number; category: string; }': title, price, category
});

Headers Validation

The headers key validates request headers. This is useful for enforcing required headers or validating header formats:

client.ts
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: {},
Type '{}' is not assignable to type '{ "x-api-key": string; "content-type": "application/json"; authorization: string; } | ((context: { baseHeaders: Record<"Authorization", `Basic ${string}` | `Bearer ${string}` | `Token ${string}` | undefined> | Record<"Content-Type", CommonContentTypes | undefined> | Record<CommonRequestHeaders, string | undefined> | Record<string, string | undefined>; }) => { "x-api-key": string; "content-type": "application/json"; authorization: string; })'.
});

Meta Validation

The meta key validates the meta option, which can be used to pass arbitrary metadata through the request lifecycle:

client.ts
import {  } from "@zayne-labs/callapi";
import {  } from "@zayne-labs/callapi/utils";
import {  } from "zod";

const  = ({
	: "https://api.example.com",

	: ({
		"/analytics": {
			: .({
				: .(),
				: .().(),
			}),
		},
	}),
});

const {  } = await ("/analytics", {
	meta: {},
Property 'trackingId' is missing in type '{}' but required in type '{ trackingId: string; userId?: string | undefined; }'.
});

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.

client.ts
import {  } from "@zayne-labs/callapi";
import {  } from "@zayne-labs/callapi/utils";
import {  } from "zod";

const  = ({
	: "https://api.example.com",

	: ({
		"/products": {
			: .({
				: .(),
				: .(),
				: .(),
			}),
		},
	}),
});

const {  } = await ("/products", {
	query: {},
Type '{}' is missing the following properties from type '{ category: string; page: number; limit: number; }': category, page, limit
});

Dynamic Path Parameters

The params schema validates URL parameters. You can define dynamic parameters in the following ways:

  1. 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.
  2. Using the params validator schema. This takes precedence over the colon syntax.
client.ts
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:

  1. Using the method validator schema
  2. 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.

client.ts
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:

api.ts
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.

client.ts
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:

  1. Accept raw input data to validate
  2. Run sync or async validation logic
  3. Transform data if needed (e.g., normalize, sanitize)
  4. Can throw errors for invalid data
  5. 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:

api.ts
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:

api.ts
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):

api.ts
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:

api.ts
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:

api.ts
import {  } from "@zayne-labs/callapi";
import {  } from "@zayne-labs/callapi/utils";
import {  } from "zod";

const  = ({
	: (
		{
			"/user": {
				: .({
					: .(),
					: .(),
					: .(),
					: .(),
				}),
			},
		},
		{
			: true,
		}
	),
});

const { ,  } = await ("/invalid-path");
Argument of type '"/invalid-path"' is not assignable to parameter of type '"/user"'.

Validation Error Handling

CallAPI throws a ValidationError when validation fails. The error includes detailed information about what failed:

client.ts
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

Edit on GitHub

Last updated on

On this page