- Created comprehensive Zod schemas (apiSchemas.ts) for: * User, Track, Playlist, Conversation, Message * Session, AuditLog, Notification * PaginationData, ApiError, ApiResponse - Added validation utilities: * validateApiResponse: Validate and normalize responses * safeValidateApiResponse: Safe validation with error handling * validateApiResponseArray: Validate arrays of items * validatePaginatedResponse: Validate paginated responses - Integrated validation into API client interceptor - Created validatedApiClient for type-safe API calls - Automatic ID normalization during validation - Comprehensive test suite (13 tests, all passing) - Ensures runtime type safety for all API responses
148 lines
4 KiB
TypeScript
148 lines
4 KiB
TypeScript
/**
|
|
* API Client with Validation
|
|
* FE-TYPE-002: Enhanced API client methods with automatic Zod validation
|
|
*
|
|
* Provides typed API client methods that automatically validate responses
|
|
* using Zod schemas.
|
|
*/
|
|
|
|
import { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
|
import { z } from 'zod';
|
|
import { apiClient } from './client';
|
|
import { safeValidateApiResponse } from '@/schemas/apiSchemas';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
/**
|
|
* Create a validated API request
|
|
*
|
|
* @param requestFn - Function that makes the API request
|
|
* @param schema - Zod schema to validate the response
|
|
* @param options - Validation options
|
|
* @returns Validated response
|
|
*/
|
|
export async function validatedRequest<T>(
|
|
requestFn: () => Promise<AxiosResponse<T>>,
|
|
schema: z.ZodSchema<T>,
|
|
options: {
|
|
strict?: boolean;
|
|
throwOnError?: boolean;
|
|
} = {},
|
|
): Promise<T> {
|
|
const { strict = false, throwOnError = false } = options;
|
|
|
|
const response = await requestFn();
|
|
const validation = safeValidateApiResponse(schema, response.data, { strict });
|
|
|
|
if (!validation.success) {
|
|
const errorMessage = `API response validation failed: ${validation.error?.errors.map(e => e.message).join(', ')}`;
|
|
logger.error('[API Validation]', {
|
|
errors: validation.error?.errors,
|
|
data: response.data,
|
|
});
|
|
|
|
if (throwOnError) {
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
// In development, log warning but continue
|
|
if (import.meta.env.DEV) {
|
|
console.warn('[API Validation Warning]', validation.error);
|
|
}
|
|
}
|
|
|
|
// Return validated data if validation succeeded, otherwise return original data
|
|
return validation.data ?? (response.data as T);
|
|
}
|
|
|
|
/**
|
|
* Validated API client methods
|
|
* Automatically validates responses using provided schemas
|
|
*/
|
|
export const validatedApiClient = {
|
|
/**
|
|
* GET request with validation
|
|
*/
|
|
get: <T = any>(
|
|
url: string,
|
|
schema: z.ZodSchema<T>,
|
|
config?: InternalAxiosRequestConfig & { _responseSchema?: z.ZodSchema },
|
|
): Promise<T> => {
|
|
return validatedRequest(
|
|
() => apiClient.get<T>(url, { ...config, _responseSchema: schema }),
|
|
schema,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* POST request with validation
|
|
*/
|
|
post: <T = any>(
|
|
url: string,
|
|
data?: any,
|
|
schema?: z.ZodSchema<T>,
|
|
config?: InternalAxiosRequestConfig & { _responseSchema?: z.ZodSchema },
|
|
): Promise<T> => {
|
|
if (!schema) {
|
|
// If no schema provided, use regular client
|
|
return apiClient.post<T>(url, data, config).then((res) => res.data);
|
|
}
|
|
return validatedRequest(
|
|
() => apiClient.post<T>(url, data, { ...config, _responseSchema: schema }),
|
|
schema,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* PUT request with validation
|
|
*/
|
|
put: <T = any>(
|
|
url: string,
|
|
data?: any,
|
|
schema?: z.ZodSchema<T>,
|
|
config?: InternalAxiosRequestConfig & { _responseSchema?: z.ZodSchema },
|
|
): Promise<T> => {
|
|
if (!schema) {
|
|
return apiClient.put<T>(url, data, config).then((res) => res.data);
|
|
}
|
|
return validatedRequest(
|
|
() => apiClient.put<T>(url, data, { ...config, _responseSchema: schema }),
|
|
schema,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* PATCH request with validation
|
|
*/
|
|
patch: <T = any>(
|
|
url: string,
|
|
data?: any,
|
|
schema?: z.ZodSchema<T>,
|
|
config?: InternalAxiosRequestConfig & { _responseSchema?: z.ZodSchema },
|
|
): Promise<T> => {
|
|
if (!schema) {
|
|
return apiClient.patch<T>(url, data, config).then((res) => res.data);
|
|
}
|
|
return validatedRequest(
|
|
() => apiClient.patch<T>(url, data, { ...config, _responseSchema: schema }),
|
|
schema,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* DELETE request with validation
|
|
*/
|
|
delete: <T = any>(
|
|
url: string,
|
|
schema?: z.ZodSchema<T>,
|
|
config?: InternalAxiosRequestConfig & { _responseSchema?: z.ZodSchema },
|
|
): Promise<T> => {
|
|
if (!schema) {
|
|
return apiClient.delete<T>(url, config).then((res) => res.data);
|
|
}
|
|
return validatedRequest(
|
|
() => apiClient.delete<T>(url, { ...config, _responseSchema: schema }),
|
|
schema,
|
|
);
|
|
},
|
|
};
|
|
|