veza/apps/web/src/services/api/clientWithValidation.ts
senke 5d4ac95356 [FE-TYPE-002] fe-type: Add Zod schemas for all API responses
- 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
2025-12-25 14:30:55 +01:00

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,
);
},
};