API Abstraction
API Abstraction
A centralized API layer provides consistent error handling, authentication, and configuration across your application.
Base API Client
// api/client.js
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api';
class ApiClient {
constructor(baseURL = API_BASE_URL) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
};
}
setAuthToken(token) {
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: { ...this.defaultHeaders, ...options.headers },
...options,
};
const response = await fetch(url, config);
if (!response.ok) {
const error = await this.handleError(response);
throw error;
}
if (response.status === 204) return null;
return response.json();
}
async handleError(response) {
const errorData = await response.json().catch(() => ({}));
return {
status: response.status,
message: errorData.message || response.statusText,
errors: errorData.errors || [],
};
}
get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data),
});
}
put(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(data),
});
}
delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
export const api = new ApiClient();
API Endpoints
// api/products.js
import { api } from './client';
export const productsApi = {
getAll: (params = {}) => {
const query = new URLSearchParams(params).toString();
return api.get(`/products?${query}`);
},
getById: (id) => api.get(`/products/${id}`),
create: (product) => api.post('/products', product),
update: (id, product) => api.put(`/products/${id}`, product),
delete: (id) => api.delete(`/products/${id}`),
search: (query) => api.get(`/products/search?q=${encodeURIComponent(query)}`),
};
// Usage in components
import { productsApi } from '../api/products';
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: productsApi.getAll,
});
}
Error Handling
Error Handling
Consistent error handling improves debugging and user experience.
Error Types
// api/errors.js
export class ApiError extends Error {
constructor(status, message, errors = []) {
super(message);
this.name = 'ApiError';
this.status = status;
this.errors = errors;
}
isUnauthorized() { return this.status === 401; }
isForbidden() { return this.status === 403; }
isNotFound() { return this.status === 404; }
isValidationError() { return this.status === 422; }
isServerError() { return this.status >= 500; }
}
export class NetworkError extends Error {
constructor(message = 'Network error occurred') {
super(message);
this.name = 'NetworkError';
}
}
export class TimeoutError extends Error {
constructor(message = 'Request timed out') {
super(message);
this.name = 'TimeoutError';
}
}
Error Boundaries with API
// components/ApiErrorBoundary.jsx
import React from 'react';
class ApiErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, errorInfo) {
console.error('API Error:', error, errorInfo);
errorReporter.captureException(error, { extra: errorInfo });
}
render() {
if (this.state.error) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error.message}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return this.props.children;
}
}
Error Handling in Hooks
// hooks/useApi.js
import { useState, useCallback } from 'react';
export function useApi(apiFn) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const execute = useCallback(async (...args) => {
try {
setLoading(true);
setError(null);
const result = await apiFn(...args);
setData(result);
return result;
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [apiFn]);
return { data, error, loading, execute };
}
// Usage
function ProductForm() {
const { execute: createProduct, loading } = useApi(productsApi.create);
const handleSubmit = async (data) => {
try {
await createProduct(data);
showToast('Product created!');
} catch (error) {
if (error.isValidationError()) {
setErrors(error.errors);
} else {
showToast('Failed to create product', 'error');
}
}
};
}
Request/Response Interceptors
Request/Response Interceptors
Interceptors allow you to transform requests and responses globally.
Axios Interceptors
// api/axios.js
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
timeout: 10000,
});
// Request interceptor
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Add request timestamp
config.metadata = { startTime: new Date() };
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
api.interceptors.response.use(
(response) => {
// Log slow requests
const duration = new Date() - response.config.metadata.startTime;
if (duration > 1000) {
console.warn(`Slow request: ${response.config.url} took ${duration}ms`);
}
return response;
},
async (error) => {
const originalRequest = error.config;
// Handle token refresh
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { data } = await axios.post('/auth/refresh', {
refreshToken: localStorage.getItem('refreshToken'),
});
localStorage.setItem('token', data.token);
originalRequest.headers.Authorization = `Bearer ${data.token}`;
return api(originalRequest);
} catch (refreshError) {
localStorage.removeItem('token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
Custom Interceptor Pattern
// api/interceptors.js
export function createInterceptors() {
const requestInterceptors = [];
const responseInterceptors = [];
return {
addRequestInterceptor: (interceptor) => {
requestInterceptors.push(interceptor);
},
addResponseInterceptor: (interceptor) => {
responseInterceptors.push(interceptor);
},
processRequest: async (config) => {
let processedConfig = config;
for (const interceptor of requestInterceptors) {
processedConfig = await interceptor(processedConfig);
}
return processedConfig;
},
processResponse: async (response) => {
let processedResponse = response;
for (const interceptor of responseInterceptors) {
processedResponse = await interceptor(processedResponse);
}
return processedResponse;
},
};
}
// Usage
const interceptors = createInterceptors();
interceptors.addRequestInterceptor((config) => {
config.headers['X-Request-ID'] = crypto.randomUUID();
return config;
});
interceptors.addResponseInterceptor((response) => {
console.log('Response:', response.status);
return response;
});
Common Interceptor Use Cases
- Authentication: Add tokens to requests
- Logging: Log requests and responses
- Metrics: Track request duration
- Retry: Retry failed requests
- Transform: Convert data formats
Practice Problems
Create a reusable React component implementing API Layer. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for API Layer using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize API Layer for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. What is the primary purpose of an API abstraction layer?
2. What does a request interceptor do?
3. How should 401 errors be handled in an API layer?
4. What is the benefit of creating separate API endpoint modules?
Flashcards
Question
What is an API abstraction layer?
Click to reveal answer
Answer
A centralized module that handles API configuration, error handling, and common request/response transformations.
Question
What do request interceptors do?
Click to reveal answer
Answer
Transform requests before they're sent, such as adding authentication headers or logging.
Question
How do you handle token refresh with interceptors?
Click to reveal answer
Answer
Catch 401 errors in response interceptor, refresh token, update headers, and retry original request.
Question
What are common interceptor use cases?
Click to reveal answer
Answer
Authentication, logging, metrics tracking, retry logic, and data transformation.
Question
What is API Layer?
Click to reveal answer
Answer
API Layer is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Create a centralized API client for consistent configuration
- 2.Define custom error classes for different error types
- 3.Use interceptors for cross-cutting concerns like auth and logging
- 4.Separate API endpoints into domain-specific modules
- 5.Always handle token refresh in response interceptors
Interview Tips
- •Explain how you would structure an API layer in a large application
- •Discuss token refresh strategies and how to handle 401 errors
- •Know the difference between request and response interceptors
Cheat Sheet
API Layer Cheat Sheet
Base Client Structure
class ApiClient {
async request(endpoint, options) { ... }
get(endpoint) { ... }
post(endpoint, data) { ... }
}
Error Types
- ApiError (4xx/5xx)
- NetworkError (connection issues)
- TimeoutError (request timeout)
Interceptor Pattern
api.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
response => response,
error => handleTokenRefresh(error)
);