Custom Axios Validation Beyond HTTP Status Codes
By default, Axios determines the success or failure of a request
based entirely on standard HTTP status codes via its
validateStatus option. However, modern APIs—such as GraphQL
endpoints or legacy REST services—often return a 200 OK
status code while embedding application-level errors or failure flags
inside the response body. This article demonstrates how to bypass
standard status-based handling and implement custom, body-level response
validation in Axios using response interceptors and runtime schema
validators.
The Limitation of
validateStatus
Axios provides a built-in validateStatus configuration
property that defines which HTTP status codes resolve or reject a
promise:
axios.get('/api/data', {
validateStatus: (status) => status >= 200 && status < 300,
});Because validateStatus only receives the numeric HTTP
status code, it cannot inspect the response headers or the payload body.
If an API returns 200 OK alongside
{ "success": false, "error": "Invalid token" },
validateStatus treats the request as successful.
Implementing Custom Validation with Response Interceptors
The standard and most robust way to validate responses based on
custom business logic or response body contents is through Axios
Response Interceptors. Interceptors run before
.then() or .catch() handlers, allowing you to
inspect the full response object, evaluate its payload, and
reject the promise when custom criteria are not met.
Basic Business Logic Validation
import axios from 'axios';
// Create an Axios instance
const apiClient = axios.create({
baseURL: 'https://api.example.com',
});
// Add a response interceptor
apiClient.interceptors.response.use(
(response) => {
const data = response.data;
// Check for custom application-level error flags in the body
if (data && data.success === false) {
const customError = new Error(data.message || 'Application Error');
customError.response = response;
customError.isCustomAppError = true;
// Reject the promise to route execution to .catch()
return Promise.reject(customError);
}
// Return the response normally if validation passes
return response;
},
(error) => {
// Handle standard network and HTTP errors (4xx, 5xx)
return Promise.reject(error);
}
);Handling GraphQL Errors Inside Interceptors
GraphQL APIs almost universally respond with HTTP
200 OK, even if query execution fails. An interceptor can
detect the errors array in the response body:
apiClient.interceptors.response.use((response) => {
if (response.data && Array.isArray(response.data.errors) && response.data.errors.length > 0) {
const graphQLError = new Error(response.data.errors[0].message);
graphQLError.errors = response.data.errors;
graphQLError.response = response;
return Promise.reject(graphQLError);
}
return response;
});Schema-Based Validation with Zod
For stricter data integrity, you can combine Axios response interceptors with runtime schema validation libraries such as Zod. This ensures the response not only lacks error flags but also strictly matches the expected structure and types.
import axios from 'axios';
import { z } from 'zod';
const apiClient = axios.create({ baseURL: 'https://api.example.com' });
apiClient.interceptors.response.use((response) => {
const schema = response.config.schema;
// If a schema is defined in the request config, validate against it
if (schema instanceof z.ZodType) {
const parseResult = schema.safeParse(response.data);
if (!parseResult.success) {
const validationError = new Error('Response validation failed');
validationError.issues = parseResult.error.issues;
validationError.response = response;
return Promise.reject(validationError);
}
// Replace raw data with sanitized, parsed data
response.data = parseResult.data;
}
return response;
});Usage Example
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// Pass schema inside the request configuration
apiClient
.get('/user/1', { schema: UserSchema })
.then((response) => {
console.log('Validated User:', response.data);
})
.catch((error) => {
if (error.issues) {
console.error('Schema Issues:', error.issues);
} else {
console.error('Request Error:', error.message);
}
});Wrapping Requests with a Custom Transport Function
If an interceptor applies too broadly across all endpoints, you can encapsulate custom validation logic in a targeted wrapper function:
async function requestWithValidation(config, validateFn) {
const response = await axios(config);
const isValid = validateFn(response.data, response);
if (!isValid) {
const error = new Error('Custom payload validation failed');
error.response = response;
throw error;
}
return response.data;
}
// Usage
try {
const data = await requestWithValidation(
{ method: 'GET', url: '/api/resource' },
(body) => body.status === 'OK' && typeof body.payload === 'object'
);
} catch (err) {
console.error('Request rejected:', err.message);
}Summary
- Use
validateStatusonly for low-level HTTP status code boundaries (e.g., accepting 304 or 404 as non-throwing). - Use Axios response interceptors to inspect body
payloads, detect embedded error flags, and trigger
Promise.reject(). - Use runtime schemas (like Zod) within interceptors to enforce structural and type guarantees directly on returned data.