Validation
Define validation schemas for your requests details and response data
Validation schemas let you pre-define URL paths and validate the shape of request and response data. This validation happens at both the type-level (TypeScript) and at runtime, helping you catch errors early, ensure data integrity, and document your API structure.
CallApi uses Standard Schema internally, allowing you to bring your own Standard Schema-compliant validator like Zod, Valibot, or ArkType.
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 to their validation rules. Each path can define validation for different parts of the request and response.
Response Validation:
data- Validates successful response dataerrorData- Validates error response data
When validation fails, CallApi throws a ValidationError with details about what went wrong.
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 adding 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 validates request body data and provides type safety for the request body:
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, 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 passes 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 before they're 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 two ways:
- Using colon syntax in the schema path (
:paramName) - Enforces types at the type level only - Using the
paramsvalidator schema - Validates at both type level and runtime (takes precedence over 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(supported:@get/,@post/,@put/,@patch/,@delete/)
When using the @method-name/ prefix, it's automatically added to request options. You can override it by explicitly passing the method option to callApi.
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 define a validation schema for a specific request 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.errorData); // Validation issues array
console.log(error.issueCause); // Which schema key caused the validation error
}Types
CallApiSchema
Prop
Type
CallApiSchemaConfig
Prop
Type
Last updated on