@pitifulhawk/flash-up
Version:
Interactive project scaffolder for modern web applications
233 lines (199 loc) • 5.93 kB
text/typescript
// 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;