UNPKG

@pitifulhawk/flash-up

Version:

Interactive project scaffolder for modern web applications

233 lines (199 loc) 5.93 kB
// Base API URL configuration const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || process.env.REACT_APP_API_URL || 'http://localhost:3000/api'; // Custom error class for API errors export class ApiError extends Error { constructor( public status: number, message: string, public data?: any ) { super(message); this.name = 'ApiError'; } } // Request configuration interface interface FetchOptions extends RequestInit { timeout?: number; params?: Record<string, any>; } // Fetch with timeout support async function fetchWithTimeout(url: string, options: FetchOptions = {}): Promise<Response> { const { timeout = 10000, params, ...fetchOptions } = options; // Add query parameters const urlWithParams = new URL(url); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { urlWithParams.searchParams.append(key, String(value)); } }); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(urlWithParams.toString(), { ...fetchOptions, signal: controller.signal, }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new ApiError(408, 'Request timeout'); } throw error; } } // Main API request function export async function apiRequest<T>( endpoint: string, options: FetchOptions = {} ): Promise<T> { const url = `${API_BASE_URL}${endpoint}`; // Add default headers const defaultHeaders: HeadersInit = { 'Content-Type': 'application/json', }; // Add auth token if available const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; if (token) { defaultHeaders.Authorization = `Bearer ${token}`; } const headers = { ...defaultHeaders, ...options.headers, }; const response = await fetchWithTimeout(url, { ...options, headers, }); // Handle non-JSON responses const contentType = response.headers.get('content-type'); const isJson = contentType?.includes('application/json'); if (!response.ok) { let errorData; try { errorData = isJson ? await response.json() : await response.text(); } catch { errorData = 'Unknown error'; } // Handle specific error cases if (response.status === 401) { if (typeof window !== 'undefined') { localStorage.removeItem('token'); window.location.href = '/login'; } } throw new ApiError( response.status, `HTTP ${response.status}: ${response.statusText}`, errorData ); } // Return response data if (isJson) { return response.json(); } else { return response.text() as unknown as T; } } // API helper object with common HTTP methods export const api = { // GET request get: <T>(endpoint: string, params?: Record<string, any>, options?: FetchOptions) => apiRequest<T>(endpoint, { ...options, method: 'GET', params, }), // POST request post: <T>(endpoint: string, data?: any, options?: FetchOptions) => apiRequest<T>(endpoint, { ...options, method: 'POST', body: data ? JSON.stringify(data) : undefined, }), // PUT request put: <T>(endpoint: string, data?: any, options?: FetchOptions) => apiRequest<T>(endpoint, { ...options, method: 'PUT', body: data ? JSON.stringify(data) : undefined, }), // PATCH request patch: <T>(endpoint: string, data?: any, options?: FetchOptions) => apiRequest<T>(endpoint, { ...options, method: 'PATCH', body: data ? JSON.stringify(data) : undefined, }), // DELETE request delete: <T>(endpoint: string, options?: FetchOptions) => apiRequest<T>(endpoint, { ...options, method: 'DELETE', }), // Upload file upload: async <T>( endpoint: string, file: File, onProgress?: (progress: number) => void, options?: Omit<FetchOptions, 'body' | 'headers'> ): Promise<T> => { const formData = new FormData(); formData.append('file', file); // Note: Progress tracking with fetch API requires additional setup // For now, we'll just upload without progress tracking if (onProgress) { console.warn('Progress tracking not implemented with fetch API'); } return apiRequest<T>(endpoint, { ...options, method: 'POST', body: formData, headers: { // Don't set Content-Type for FormData, let browser set it ...options?.headers, }, }); }, }; // Utility functions for common API patterns export const apiUtils = { // Create a paginated request paginated: <T>(endpoint: string, page: number = 1, limit: number = 10) => api.get<{ data: T[]; total: number; page: number; limit: number }>( endpoint, { page, limit } ), // Create a search request search: <T>(endpoint: string, query: string, filters?: Record<string, any>) => api.get<T[]>(endpoint, { q: query, ...filters }), // Retry a request with exponential backoff retry: async <T>( requestFn: () => Promise<T>, maxRetries: number = 3, baseDelay: number = 1000 ): Promise<T> => { let lastError: Error; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await requestFn(); } catch (error) { lastError = error as Error; if (attempt === maxRetries) { break; } // Exponential backoff const delay = baseDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError!; }, }; export default api;