Validate Axios with Zod and Yup Schemas
Validating HTTP requests and responses ensures that your application communicates safely with external APIs by enforcing strict data contracts at runtime. While TypeScript provides compile-time safety, runtime payloads can diverge from expected types. This guide explains how to enforce request and response schema validation using Zod and Yup alongside the Axios HTTP client through helper wrappers and Axios interceptors.
Why Validate Axios Payloads?
Axios automatically handles JSON serialization and deserialization, but it does not verify if the returned or dispatched data matches your application's expectations. Integrating schema validation libraries such as Zod or Yup solves two critical problems:
- Defensive Ingestion: Prevents malformed API response data from propagating through your UI or business logic.
- Defensive Mutation: Validates request payloads before sending them over the network, avoiding unnecessary failed requests.
Schema Validation with Zod
Zod uses TypeScript-first schema definitions with automatic type inference.
1. Validating Responses with a Wrapper Function
The simplest approach is wrapping Axios calls and parsing the data
with Zod's .parse() or .safeParse()
method.
import axios from 'axios';
import { z } from 'zod';
// Define the schema
export const UserResponseSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
});
// Infer TypeScript type directly from the schema
export type UserResponse = z.infer<typeof UserResponseSchema>;
// API call with validation
export async function getUser(userId: number): Promise<UserResponse> {
const response = await axios.get(`https://api.example.com/users/${userId}`);
// Throws a ZodError if validation fails
return UserResponseSchema.parse(response.data);
}2. Validating Requests with Zod
Validate user input or state before passing it to Axios:
export const CreateUserRequestSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>;
export async function createUser(payload: CreateUserRequest) {
// Validate the outgoing payload
const validatedPayload = CreateUserRequestSchema.parse(payload);
const response = await axios.post('https://api.example.com/users', validatedPayload);
return response.data;
}Schema Validation with Yup
Yup is another popular validation library frequently used in form validation and API layer integrity.
1. Validating Responses with Yup
Yup requires schemas to run through asynchronous validation functions
like .validate():
import axios from 'axios';
import * as yup from 'yup';
export const ProductResponseSchema = yup.object({
id: yup.number().required(),
title: yup.string().required(),
price: yup.number().positive().required(),
});
export type ProductResponse = yup.InferType<typeof ProductResponseSchema>;
export async function getProduct(productId: number): Promise<ProductResponse> {
const response = await axios.get(`https://api.example.com/products/${productId}`);
// Validate and strip unknown properties
return await ProductResponseSchema.validate(response.data, {
stripUnknown: true,
abortEarly: false,
});
}2. Validating Requests with Yup
export const UpdateProductRequestSchema = yup.object({
title: yup.string().min(3),
price: yup.number().positive(),
});
export type UpdateProductRequest = yup.InferType<typeof UpdateProductRequestSchema>;
export async function updateProduct(productId: number, payload: UpdateProductRequest) {
const validatedData = await UpdateProductRequestSchema.validate(payload, {
abortEarly: false,
});
const response = await axios.put(
`https://api.example.com/products/${productId}`,
validatedData
);
return response.data;
}Centralizing Validation Using Axios Interceptors
To avoid writing validation calls in every service function, you can attach schemas to custom Axios request configs using interceptors.
Defining Custom Config Types
Extend Axios to accept validation schemas:
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { ZodSchema } from 'zod';
declare module 'axios' {
export interface AxiosRequestConfig {
requestSchema?: ZodSchema;
responseSchema?: ZodSchema;
}
}Configuring the Interceptors
const apiClient: AxiosInstance = axios.create({
baseURL: 'https://api.example.com',
});
// Request Interceptor: Validates payload before sending
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
if (config.requestSchema && config.data) {
config.data = config.requestSchema.parse(config.data);
}
return config;
});
// Response Interceptor: Validates received data
apiClient.interceptors.response.use((response: AxiosResponse) => {
if (response.config.responseSchema) {
response.data = response.config.responseSchema.parse(response.data);
}
return response;
});Executing Validated Requests
import { z } from 'zod';
const TodoSchema = z.object({
id: z.number(),
title: z.string(),
completed: z.boolean(),
});
const CreateTodoSchema = z.object({
title: z.string().min(1),
});
export async function createTodoItem(title: string) {
const response = await apiClient.post(
'/todos',
{ title },
{
requestSchema: CreateTodoSchema,
responseSchema: TodoSchema,
}
);
return response.data; // Fully typed and validated
}Error Handling
Validation failures throw specific errors depending on the library:
- Zod: Throws
ZodErrorcontaining anissuesarray detailing failed paths and message codes. - Yup: Throws
ValidationErrorcontaining anerrorsarray of human-readable messages.
import { ZodError } from 'zod';
import { ValidationError } from 'yup';
try {
await createTodoItem('');
} catch (error) {
if (error instanceof ZodError) {
console.error('Zod Validation Failed:', error.flatten());
} else if (error instanceof ValidationError) {
console.error('Yup Validation Failed:', error.errors);
} else if (axios.isAxiosError(error)) {
console.error('HTTP Request Failed:', error.response?.status);
}
}Using this pattern separates network errors from data contract mismatches, making it easier to pinpoint whether an issue originated from the network layer, client-side input, or backend schema changes.