veza/apps/web/src/services/api/clientWithValidation.ts
senke e5f842c527 [FE-TYPE-003] fe-type: Add Zod schemas for all API requests
- Created comprehensive Zod schemas (apiRequestSchemas.ts) for:
  * LoginRequest, RegisterRequest, CreateUserRequest
  * UpdateUserRequest, UpdateProfileRequest
  * SendMessageRequest, UpdateMessageRequest
  * CreateConversationRequest, UpdateConversationRequest
  * UploadTrackRequest, UpdateTrackRequest
  * PaginationParams and list/search request types
- Added validation utilities:
  * validateApiRequest: Validate requests before sending
  * safeValidateApiRequest: Safe validation with error handling
  * validateApiRequestWithError: Validation with custom error handler
- Integrated validation into API client request interceptor
- Enhanced validatedApiClient with request validation support
- Automatic validation prevents invalid requests from being sent
- Comprehensive test suite (19 tests, all passing)
- Ensures runtime type safety for all API requests
2025-12-25 14:36:32 +01:00

190 lines
5.3 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 { AxiosResponse } from 'axios';
import { z } from 'zod';
import { apiClient } from './client';
import { safeValidateApiResponse } from '@/schemas/apiSchemas';
import { safeValidateApiRequest } from '@/schemas/apiRequestSchemas';
import { logger } from '@/utils/logger';
// Extend InternalAxiosRequestConfig to include validation schemas
interface ValidatedRequestConfig {
_requestSchema?: z.ZodSchema;
_responseSchema?: z.ZodSchema;
[key: string]: any;
}
/**
* 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: {
throwOnError?: boolean;
} = {},
): Promise<T> {
const { throwOnError = false } = options;
const response = await requestFn();
const validation = safeValidateApiResponse(schema, response.data);
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?: ValidatedRequestConfig,
): Promise<T> => {
return validatedRequest(
() => apiClient.get<T>(url, { ...config, _responseSchema: schema } as any),
schema,
);
},
/**
* GET request with request params validation
*/
getWithParams: <T = any, P = any>(
url: string,
paramsSchema: z.ZodSchema<P>,
responseSchema: z.ZodSchema<T>,
params?: P,
config?: ValidatedRequestConfig,
): Promise<T> => {
// Validate params
if (params) {
const validation = safeValidateApiRequest(paramsSchema, params);
if (!validation.success) {
throw new Error(`Request params validation failed: ${validation.error?.errors.map(e => e.message).join(', ')}`);
}
params = validation.data;
}
return validatedRequest(
() => apiClient.get<T>(url, { ...config, params, _responseSchema: responseSchema } as any),
responseSchema,
);
},
/**
* POST request with validation
*/
post: <T = any, D = any>(
url: string,
data?: D,
requestSchema?: z.ZodSchema<D>,
responseSchema?: z.ZodSchema<T>,
config?: ValidatedRequestConfig,
): Promise<T> => {
// Validate request data if schema provided
let validatedData = data;
if (requestSchema && data !== undefined && data !== null) {
const validation = safeValidateApiRequest(requestSchema, data);
if (!validation.success) {
throw new Error(`Request validation failed: ${validation.error?.errors.map(e => e.message).join(', ')}`);
}
validatedData = validation.data;
}
if (!responseSchema) {
// If no response schema provided, use regular client
return apiClient.post<T>(url, validatedData, { ...config, _requestSchema: requestSchema } as any).then((res) => res.data);
}
return validatedRequest(
() => apiClient.post<T>(url, validatedData, { ...config, _requestSchema: requestSchema, _responseSchema: responseSchema } as any),
responseSchema,
);
},
/**
* PUT request with validation
*/
put: <T = any>(
url: string,
data?: any,
schema?: z.ZodSchema<T>,
config?: ValidatedRequestConfig,
): Promise<T> => {
if (!schema) {
return apiClient.put<T>(url, data, config as any).then((res) => res.data);
}
return validatedRequest(
() => apiClient.put<T>(url, data, { ...config, _responseSchema: schema } as any),
schema,
);
},
/**
* PATCH request with validation
*/
patch: <T = any>(
url: string,
data?: any,
schema?: z.ZodSchema<T>,
config?: ValidatedRequestConfig,
): Promise<T> => {
if (!schema) {
return apiClient.patch<T>(url, data, config as any).then((res) => res.data);
}
return validatedRequest(
() => apiClient.patch<T>(url, data, { ...config, _responseSchema: schema } as any),
schema,
);
},
/**
* DELETE request with validation
*/
delete: <T = any>(
url: string,
schema?: z.ZodSchema<T>,
config?: ValidatedRequestConfig,
): Promise<T> => {
if (!schema) {
return apiClient.delete<T>(url, config as any).then((res) => res.data);
}
return validatedRequest(
() => apiClient.delete<T>(url, { ...config, _responseSchema: schema } as any),
schema,
);
},
};