_private/qwestly-docs/Engineering/API-Request-Client.md
Table of Contents
API Request Client Documentation
This is a TypeScript utility that provides a clean, type-safe interface for making HTTP requests to your API endpoints. It includes built-in error handling, response typing, request body typing, and configuration options.
Features
- Type-safe request and response handling with dual generic support
- Consistent error handling across all requests
- Support for JSON and FormData payloads
- Configurable base URL
- Built-in TypeScript generics for both response and request body typing
- Automatic Content-Type header management
Installation
The API client is available as part of the core utilities. Import it directly from the utils directory:
import ApiRequest from '@/lib/api-request';
Basic Usage
Generic Type Parameters
The API client supports two generic type parameters:
- TResponse: Type for the response data
- TRequest: Type for the request body (for POST/PUT/PATCH methods)
// GET requests: ApiRequest.get<TResponse>(path)
const { data, error } = await ApiRequest.get<User>('users/123');
// POST/PUT/PATCH requests: ApiRequest.method<TResponse, TRequest>(path, data)
const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', userData);
GET Request
// GET: /api/users
const { data, error } = await ApiRequest.get('users');
// With type definition
interface User {
id: string;
name: string;
email: string;
}
const { data, error } = await ApiRequest.get<User>('users/123');
if (error) {
console.error('Failed to fetch user:', error);
return;
}
console.log('User data:', data); // data is typed as User | null
POST Request
// POST: /api/users
interface CreateUserRequest {
name: string;
email: string;
role: 'admin' | 'user';
}
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: string;
}
const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', {
name: 'John Doe',
email: 'john@example.com',
role: 'user'
});
// With FormData
const formData = new FormData();
formData.append('file', file);
const { data, error } = await ApiRequest.post<UploadResponse, FormData>('upload', formData);
Other HTTP Methods
// PATCH: /api/users/123
interface UpdateUserRequest {
name?: string;
email?: string;
}
const { data, error } = await ApiRequest.patch<User, UpdateUserRequest>('users/123', {
name: 'Updated Name'
});
// PUT: /api/users/456
interface UpdateResourceRequest {
status: 'active' | 'inactive';
}
const { data, error } = await ApiRequest.put<Resource, UpdateResourceRequest>('resources/456', {
status: 'active'
});
// DELETE: /api/users/123
const { data, error } = await ApiRequest.delete('users/123');
Configuration
You can configure the base URL:
ApiRequest.configure({
baseURL: '/custom-api'
});
Type Definitions
APIResponse
interface APIResponse<TResponse = any> {
data: TResponse | null;
error: APIError | null;
}
APIError
interface APIError extends Error {
status?: number; // HTTP status code
data?: unknown; // Error response data from server
}
APIConfig
interface APIConfig {
baseURL: string; // Base URL for all API requests
}
Error Handling
The library provides consistent error handling across all requests:
const { data, error } = await ApiRequest.get<User>('users/123');
if (error) {
if (error.status === 404) {
console.error('User not found');
} else if (error.status === 403) {
console.error('Permission denied');
} else {
console.error('An error occurred:', error.message);
}
// Access additional error data if available
if (error.data) {
console.error('Server error details:', error.data);
}
return;
}
Content-Type Handling
- JSON requests automatically include
Content-Type: application/json - FormData requests automatically omit Content-Type to allow the browser to set the correct multipart boundary
- All responses are automatically parsed as JSON
Best Practices
-
Always Type Your Responses and Request Bodies
// ❌ Avoid - No type safety const { data } = await ApiRequest.get('users'); const { data } = await ApiRequest.post('users', userData); // ✅ Better - Full type safety const { data } = await ApiRequest.get<User[]>('users'); const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', userData); -
Always Handle Errors
// ❌ Avoid const { data } = await ApiRequest.post<User, CreateUserRequest>('users', userData); // ✅ Better const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', userData); if (error) { // Handle error appropriately return; } -
Use Type Definitions for Request Bodies
interface CreateUserRequest { name: string; email: string; role: 'admin' | 'user'; } const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', { name: 'John Doe', email: 'john@example.com', role: 'user' }); -
Define Clear Interfaces for Both Request and Response
// Request interface interface UpdateCandidateRequest { name?: string; email?: string; linkedin_user_name?: string; } // Response interface interface Candidate { id: string; name: string; email: string; linkedin_user_name?: string; created_at: string; } // Full type safety const { data, error } = await ApiRequest.patch<Candidate, UpdateCandidateRequest>( `enhanced/candidates/${userId}`, updateData );
Error Logging
All API errors are automatically logged to the console with the following information:
- HTTP method
- Request path
- Error details
This helps with debugging and monitoring API issues in development.
Notes
- All methods return a Promise with an
APIResponseobject - The base URL defaults to
/apibut can be configured - Request URLs are automatically prefixed with the base URL
- Error responses include both the error message and the original response data when available
- Required: All API request methods MUST include generic type parameters for full type safety