Overview
CallApi provides powerful schema validation capabilities using the Standard Schema specification. This allows you to validate request and response data at runtime with any compatible validation library.
Standard Schema Support
CallApi implements Standard Schema v1, which means it works seamlessly with popular validation libraries:
Zod - TypeScript-first schema validation
Valibot - Lightweight schema validation
Yup - JavaScript schema builder
ArkType - TypeScript validation with runtime type inference
Any library implementing the Standard Schema interface
Standard Schema Interface
The Standard Schema interface requires a validation function that returns a result:
interface StandardSchemaV1 < Input = unknown , Output = Input > {
readonly '~standard' : {
readonly vendor : string ;
readonly version : 1 ;
readonly validate : (
value : unknown
) => Promise < Result < Output >> | Result < Output >;
};
}
type Result < Output > =
| { issues ?: undefined ; value : Output }
| { issues : readonly Issue []; value ?: undefined };
Source: types/standard-schema.ts:52-69
Schema Types
CallApi supports validation for different parts of the request/response cycle:
body - Validate request body data
headers - Validate request headers
params - Validate URL path parameters
query - Validate query string parameters
method - Validate HTTP method
auth - Validate authentication options
data - Validate successful response data
errorData - Validate error response data
meta - Validate metadata options
Source: validation.ts:163-205
Basic Usage
Using Zod
import { z } from 'zod' ;
import { createFetchClient } from 'callapi' ;
const UserSchema = z . object ({
id: z . number (),
name: z . string (),
email: z . string (). email (),
});
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
schema: {
routes: {
'/users/:id' : {
data: UserSchema ,
params: z . object ({
id: z . string (). regex ( / ^ \d + $ / ),
}),
},
},
},
});
// TypeScript knows the return type matches UserSchema
const { data } = await callApi ( '/users/123' );
Using Valibot
import * as v from 'valibot' ;
import { createFetchClient } from 'callapi' ;
const PostSchema = v . object ({
id: v . number (),
title: v . string (),
body: v . string (),
userId: v . number (),
});
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
schema: {
routes: {
'POST /posts' : {
body: PostSchema ,
data: PostSchema ,
},
},
},
});
const { data } = await callApi ( 'POST /posts' , {
body: {
title: 'My Post' ,
body: 'Post content' ,
userId: 1 ,
},
});
Custom Validator Functions
You can also use custom validation functions:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
schema: {
routes: {
'/status' : {
data : ( value ) => {
if ( typeof value !== 'object' || ! value ) {
throw new Error ( 'Expected object' );
}
if ( ! ( 'status' in value )) {
throw new Error ( 'Missing status field' );
}
return value ;
},
},
},
},
});
Source: validation.ts:55-64
Schema Configuration
Base URL and Prefixes
Control how schemas map to routes:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
schema: {
config: {
baseURL: '/api/v1' ,
prefix: '/api' ,
},
routes: {
'/users' : UserSchema ,
},
},
});
// Calls https://api.example.com/api/v1/users
// But matches schema for '/users'
await callApi ( '/api/users' );
Source: validation.ts:117-143
baseURL stringThe base URL for schema routes. By default, uses the client’s baseURL. prefix stringA URL prefix that will be substituted for the baseURL at runtime. Keeps route definitions concise. strict booleanWhen true, only routes explicitly defined in the schema are allowed. Undefined routes cause validation errors. schema : {
config : {
strict : true ,
},
routes : {
'/users' : UserSchema ,
},
}
// ✅ Allowed
await callApi ( '/users' );
// ❌ Runtime error: Route not in schema
await callApi ( '/posts' );
disableRuntimeValidation boolean | BooleanObjectDisables runtime validation for specific schema fields: schema : {
config : {
// Disable all validation
disableRuntimeValidation : true ,
// Or disable specific fields
disableRuntimeValidation : {
data : true ,
params : false ,
},
},
}
disableRuntimeValidationTransform boolean | BooleanObjectUses original input instead of validated/transformed output: schema : {
config : {
// Use raw input, ignore transformations
disableRuntimeValidationTransform : true ,
},
}
Source: validation.ts:117-157
Route Patterns
HTTP Method Prefixes
Define schemas specific to HTTP methods:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
schema: {
routes: {
'@get/users' : {
data: UserListSchema ,
},
'@post/users' : {
body: CreateUserSchema ,
data: UserSchema ,
},
'@delete/users/:id' : {
params: z . object ({ id: z . string () }),
},
},
},
});
Source: validation.ts:207-211
Fallback Schema
Define a default schema for all routes:
import { fallBackRouteSchemaKey } from 'callapi' ;
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
schema: {
routes: {
[fallBackRouteSchemaKey]: {
headers: CommonHeadersSchema ,
errorData: ErrorSchema ,
},
'/users' : {
data: UserSchema ,
// Inherits headers and errorData from fallback
},
},
},
});
Source: validation.ts:407-414
Dynamic Schemas
Use functions to compute schemas based on context:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
schema: {
routes: {
'/users' : UserSchema ,
},
},
});
await callApi ( '/posts' , {
schema : ({ currentRouteSchema , baseSchemaRoutes }) => {
// Extend base schema for this request
return {
... currentRouteSchema ,
data: PostSchema ,
};
},
});
Source: validation.ts:416-426
Validation Error Handling
When validation fails, CallApi throws a ValidationError:
import { ValidationError } from 'callapi' ;
try {
await callApi ( '/users/invalid' );
} catch ( error ) {
if ( error instanceof ValidationError ) {
console . log ( 'Validation failed:' , error . issueCause );
console . log ( 'Issues:' , error . issues );
console . log ( 'Response:' , error . response );
}
}
Validation Error Hook
Use the onValidationError hook to handle validation errors:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onValidationError : ({ error , request , options }) => {
console . error ( 'Validation failed:' , error . issueCause );
console . error ( 'Issues:' , error . issues );
// Send to error tracking
trackError ({
type: 'validation' ,
cause: error . issueCause ,
issues: error . issues ,
url: options . fullURL ,
});
},
});
Source: validation.ts:102-111
Type Inference
CallApi automatically infers types from your schemas:
import { z } from 'zod' ;
const UserSchema = z . object ({
id: z . number (),
name: z . string (),
});
const callApi = createFetchClient ({
schema: {
routes: {
'/users/:id' : {
data: UserSchema ,
},
},
},
});
// TypeScript infers: { id: number; name: string }
const { data } = await callApi ( '/users/123' );
Schema Type Helpers
import type { InferSchemaOutput , InferSchemaInput } from 'callapi' ;
type UserOutput = InferSchemaOutput < typeof UserSchema >;
type UserInput = InferSchemaInput < typeof UserSchema >;
Source: validation.ts:33-53
Best Practices
Create shared schema definitions: const PaginationParams = z . object ({
page: z . number (). int (). positive (),
limit: z . number (). int (). min ( 1 ). max ( 100 ),
});
const ErrorResponse = z . object ({
message: z . string (),
code: z . string (),
});
const callApi = createFetchClient ({
schema: {
routes: {
'/users' : {
query: PaginationParams ,
errorData: ErrorResponse ,
},
'/posts' : {
query: PaginationParams ,
errorData: ErrorResponse ,
},
},
},
});
Use Strict Mode for Type Safety
Enable strict mode to catch typos and undefined routes: const callApi = createFetchClient ({
schema: {
config: { strict: true },
routes: {
'/users' : UserSchema ,
},
},
});
// TypeScript error + runtime error
await callApi ( '/userz' ); // Typo caught!
Validate request data before sending: schema : {
routes : {
'POST /users' : {
body: z . object ({
email: z . string (). email (),
age: z . number (). min ( 18 ),
}),
},
},
}
// Validation fails before request is sent
await callApi ( 'POST /users' , {
body: { email: 'invalid' , age: 10 },
});
Handle Validation in Hooks
Centralize validation error handling: const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onValidationError : ({ error }) => {
// Show user-friendly messages
toast . error ( `Invalid ${ error . issueCause } : ${ error . message } ` );
// Log for debugging
logger . error ( 'Validation failed' , {
cause: error . issueCause ,
issues: error . issues ,
});
},
});
Disable validation in production if needed using disableRuntimeValidation
Use compiled schemas with libraries that support compilation (e.g., Zod’s .preprocess())
Cache schema instances to avoid recreation on each request
Consider transformation costs when using disableRuntimeValidationTransform
Validation adds runtime overhead. For performance-critical paths, consider using TypeScript-only validation or disabling runtime validation after initial development.