Master the art of API integration and build dynamic, data-driven web applications
Table of Contents
- What Are APIs and Why Do They Matter?
- Types of APIs Frontend Developers Should Know
- HTTP Methods and Status Codes
- Making API Calls: From Fetch to Axios
- Authentication and Security
- Error Handling and Best Practices
- State Management with APIs
- API Integration Patterns
- Performance Optimization
- Testing API Integrations
- Popular APIs for Frontend Projects
- Real-World Examples
What Are APIs and Why Do They Matter? {#what-are-apis}
API (Application Programming Interface) is a set of rules and protocols that allows different software applications to communicate with each other. For frontend developers, APIs are the bridge between your user interface and backend services, enabling you to create dynamic, data-driven applications.
Why APIs Are Essential for Frontend Developers
Data Integration: APIs allow your frontend to fetch, display, and manipulate data from various sources without managing databases directly.
Separation of Concerns: APIs enable a clean separation between frontend and backend, allowing teams to work independently and use different technologies.
Scalability: Well-designed APIs can handle multiple frontend applications (web, mobile, desktop) from a single backend.
Third-Party Services: APIs let you integrate powerful third-party services like payment processors, social media platforms, and cloud services.
Types of APIs Frontend Developers Should Know {#types-of-apis}
1. REST APIs
Representational State Transfer is the most common API architecture. REST APIs use standard HTTP methods and are stateless.
Characteristics:
- Uses HTTP methods (GET, POST, PUT, DELETE)
- Stateless communication
- Resource-based URLs
- JSON data format (typically)
Example URL Structure:
GET /api/users # Get all users
GET /api/users/123 # Get user with ID 123
POST /api/users # Create a new user
PUT /api/users/123 # Update user 123
DELETE /api/users/123 # Delete user 123
2. GraphQL APIs
GraphQL provides a more flexible alternative to REST, allowing clients to request exactly the data they need.
Benefits:
- Single endpoint for all operations
- Precise data fetching (no over-fetching)
- Strong type system
- Real-time subscriptions
Example Query:
query {
user(id: "123") {
name
email
posts {
title
publishedAt
}
}
}
3. WebSocket APIs
For real-time communication between client and server.
Use Cases:
- Chat applications
- Live updates
- Gaming
- Collaborative tools
4. Server-Sent Events (SSE)
One-way communication from server to client for live updates.
Use Cases:
- Live notifications
- Stock price updates
- Live sports scores
HTTP Methods and Status Codes {#http-methods}
Essential HTTP Methods
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve data | ✅ | ✅ |
| POST | Create new resource | ❌ | ❌ |
| PUT | Update/replace resource | ✅ | ❌ |
| PATCH | Partially update resource | ❌ | ❌ |
| DELETE | Remove resource | ✅ | ❌ |
Key HTTP Status Codes
Success (2xx):
200 OK– Request successful201 Created– Resource created successfully204 No Content– Success, but no content to return
Client Errors (4xx):
400 Bad Request– Invalid request syntax401 Unauthorized– Authentication required403 Forbidden– Access denied404 Not Found– Resource not found429 Too Many Requests– Rate limit exceeded
Server Errors (5xx):
500 Internal Server Error– Generic server error502 Bad Gateway– Invalid response from upstream server503 Service Unavailable– Server temporarily unavailable
Making API Calls: From Fetch to Axios {#making-api-calls}
Using the Fetch API
The modern, native way to make HTTP requests in JavaScript.
Basic GET Request:
async function fetchUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
POST Request with JSON:
async function createUser(userData) {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const newUser = await response.json();
return newUser;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}
Using Axios
A popular HTTP client library with additional features.
Installation:
npm install axios
Basic Usage:
import axios from 'axios';
// GET request
const fetchUsers = async () => {
try {
const response = await axios.get('/api/users');
return response.data;
} catch (error) {
console.error('Error:', error.response?.data || error.message);
throw error;
}
};
// POST request
const createUser = async (userData) => {
try {
const response = await axios.post('/api/users', userData);
return response.data;
} catch (error) {
console.error('Error:', error.response?.data || error.message);
throw error;
}
};
Axios Configuration:
// Create an instance with default config
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
}
});
// Request interceptor
apiClient.interceptors.request.use(
(config) => {
// Add auth token
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized access
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
Authentication and Security {#authentication}
1. API Keys
Simple authentication method where a unique key identifies the client.
const response = await fetch('/api/data', {
headers: {
'X-API-Key': 'your-api-key-here'
}
});
Security Note: Never expose API keys in client-side code for sensitive APIs. Use environment variables and proxy through your backend.
2. Bearer Tokens (JWT)
JSON Web Tokens are a popular method for stateless authentication.
const token = localStorage.getItem('authToken');
const response = await fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}`
}
});
JWT Token Management:
class AuthService {
static setToken(token) {
localStorage.setItem('authToken', token);
}
static getToken() {
return localStorage.getItem('authToken');
}
static removeToken() {
localStorage.removeItem('authToken');
}
static isTokenValid() {
const token = this.getToken();
if (!token) return false;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp > Date.now() / 1000;
} catch {
return false;
}
}
}
3. OAuth 2.0
For integrating with third-party services like Google, Facebook, GitHub.
// Example: Google OAuth
const initiateGoogleAuth = () => {
const params = new URLSearchParams({
client_id: 'your-client-id',
redirect_uri: 'http://localhost:3000/callback',
scope: 'openid profile email',
response_type: 'code',
state: generateRandomString()
});
window.location.href = `https://accounts.google.com/oauth/authorize?${params}`;
};
4. CORS (Cross-Origin Resource Sharing)
Understanding CORS is crucial for frontend API integration.
Common CORS Issues and Solutions:
// If you control the backend, add CORS headers:
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Methods: GET, POST, PUT, DELETE
// Access-Control-Allow-Headers: Content-Type, Authorization
// For development, you can use a proxy in package.json:
{
"name": "my-app",
"proxy": "http://localhost:5000",
// ...other config
}
Error Handling and Best Practices {#error-handling}
Comprehensive Error Handling
class ApiError extends Error {
constructor(message, status, data) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}
const apiCall = async (url, options = {}) => {
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
const data = await response.json();
if (!response.ok) {
throw new ApiError(
data.message || 'An error occurred',
response.status,
data
);
}
return data;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
// Network or other errors
throw new ApiError(
'Network error occurred',
0,
{ originalError: error }
);
}
};
// Usage
const handleApiCall = async () => {
try {
const data = await apiCall('/api/users');
// Handle success
} catch (error) {
if (error instanceof ApiError) {
switch (error.status) {
case 400:
showError('Invalid request. Please check your input.');
break;
case 401:
redirectToLogin();
break;
case 403:
showError('You do not have permission to perform this action.');
break;
case 404:
showError('The requested resource was not found.');
break;
case 429:
showError('Too many requests. Please try again later.');
break;
case 500:
showError('Server error. Please try again later.');
break;
default:
showError('An unexpected error occurred.');
}
} else {
showError('Network error. Please check your connection.');
}
}
};
Retry Logic with Exponential Backoff
const retryWithBackoff = async (fn, maxRetries = 3, baseDelay = 1000) => {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await fn();
} catch (error) {
attempt++;
if (attempt >= maxRetries) {
throw error;
}
// Only retry on specific errors
if (error.status && error.status < 500 && error.status !== 429) {
throw error;
}
const delay = baseDelay * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};
// Usage
const fetchDataWithRetry = () => retryWithBackoff(
() => apiCall('/api/data'),
3,
1000
);
State Management with APIs {#state-management}
React with Custom Hooks
import { useState, useEffect, useCallback } from 'react';
// Custom hook for API data fetching
const useApi = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await apiCall(url, options);
setData(response);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, [url, JSON.stringify(options)]);
useEffect(() => {
fetchData();
}, [fetchData]);
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
};
// Usage in component
const UsersList = () => {
const { data: users, loading, error, refetch } = useApi('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={refetch}>Refresh</button>
{users?.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
Using React Query (TanStack Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetch users
const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: () => apiCall('/api/users'),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
// Create user
const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userData) => apiCall('/api/users', {
method: 'POST',
body: JSON.stringify(userData),
}),
onSuccess: () => {
// Invalidate and refetch users
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
};
// Component
const UsersPage = () => {
const { data: users, isLoading, error } = useUsers();
const createUserMutation = useCreateUser();
const handleCreateUser = (userData) => {
createUserMutation.mutate(userData);
};
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{/* User list and create form */}
</div>
);
};
API Integration Patterns {#integration-patterns}
1. Repository Pattern
class UserRepository {
constructor(apiClient) {
this.api = apiClient;
}
async getAll() {
return this.api.get('/users');
}
async getById(id) {
return this.api.get(`/users/${id}`);
}
async create(userData) {
return this.api.post('/users', userData);
}
async update(id, userData) {
return this.api.put(`/users/${id}`, userData);
}
async delete(id) {
return this.api.delete(`/users/${id}`);
}
}
// Usage
const userRepo = new UserRepository(apiClient);
const users = await userRepo.getAll();
2. Service Layer Pattern
class UserService {
constructor(userRepository) {
this.userRepo = userRepository;
}
async getUsersWithPosts() {
const users = await this.userRepo.getAll();
// Enhance users with additional data
return Promise.all(
users.map(async (user) => {
const posts = await this.postRepo.getByUserId(user.id);
return { ...user, posts };
})
);
}
async createUserWithValidation(userData) {
// Business logic validation
if (!userData.email) {
throw new Error('Email is required');
}
// Check if user already exists
const existingUsers = await this.userRepo.getAll();
if (existingUsers.some(user => user.email === userData.email)) {
throw new Error('User with this email already exists');
}
return this.userRepo.create(userData);
}
}
3. Facade Pattern
class ApiManager {
constructor() {
this.userService = new UserService();
this.postService = new PostService();
this.authService = new AuthService();
}
// Simplified interface for complex operations
async getDashboardData(userId) {
const [user, posts, notifications] = await Promise.all([
this.userService.getById(userId),
this.postService.getByUserId(userId),
this.notificationService.getUnread(userId)
]);
return {
user,
recentPosts: posts.slice(0, 5),
unreadCount: notifications.length
};
}
}
Performance Optimization {#performance-optimization}
1. Caching Strategies
// Simple in-memory cache
class ApiCache {
constructor(ttl = 300000) { // 5 minutes default
this.cache = new Map();
this.ttl = ttl;
}
set(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return item.data;
}
clear() {
this.cache.clear();
}
}
const cache = new ApiCache();
const cachedApiCall = async (url, options = {}) => {
const cacheKey = `${url}-${JSON.stringify(options)}`;
// Try cache first
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
// Make API call
const data = await apiCall(url, options);
// Cache the result
cache.set(cacheKey, data);
return data;
};
2. Request Deduplication
class RequestDeduplicator {
constructor() {
this.pendingRequests = new Map();
}
async request(url, options = {}) {
const key = `${url}-${JSON.stringify(options)}`;
// If request is already pending, return the same promise
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key);
}
// Create new request
const request = apiCall(url, options).finally(() => {
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, request);
return request;
}
}
const deduplicator = new RequestDeduplicator();
3. Pagination and Infinite Scroll
const useInfiniteData = (endpoint, limit = 20) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const loadMore = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const response = await apiCall(
`${endpoint}?page=${page}&limit=${limit}`
);
if (response.data.length < limit) {
setHasMore(false);
}
setData(prev => [...prev, ...response.data]);
setPage(prev => prev + 1);
} catch (error) {
console.error('Error loading more data:', error);
} finally {
setLoading(false);
}
}, [endpoint, limit, page, loading, hasMore]);
useEffect(() => {
loadMore();
}, []);
return { data, loading, hasMore, loadMore };
};
4. Request Batching
class RequestBatcher {
constructor(batchSize = 10, delay = 100) {
this.batchSize = batchSize;
this.delay = delay;
this.queue = [];
this.timeout = null;
}
async batch(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
if (this.queue.length >= this.batchSize) {
this.processBatch();
} else if (!this.timeout) {
this.timeout = setTimeout(() => this.processBatch(), this.delay);
}
});
}
async processBatch() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
const batch = this.queue.splice(0, this.batchSize);
if (batch.length === 0) return;
try {
const requests = batch.map(item => item.request);
const results = await Promise.allSettled(
requests.map(req => apiCall(req.url, req.options))
);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
batch[index].resolve(result.value);
} else {
batch[index].reject(result.reason);
}
});
} catch (error) {
batch.forEach(item => item.reject(error));
}
}
}
Testing API Integrations {#testing}
1. Mocking API Calls
// Using Jest
import { jest } from '@jest/globals';
// Mock the API module
jest.mock('../api/userApi', () => ({
getUsers: jest.fn(),
createUser: jest.fn(),
}));
import { getUsers, createUser } from '../api/userApi';
describe('User Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('displays users when API call succeeds', async () => {
const mockUsers = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' }
];
getUsers.mockResolvedValue(mockUsers);
render(<UsersList />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
});
});
test('displays error when API call fails', async () => {
getUsers.mockRejectedValue(new Error('API Error'));
render(<UsersList />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
2. Using Mock Service Worker (MSW)
// handlers.js
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' }
])
);
}),
rest.post('/api/users', async (req, res, ctx) => {
const newUser = await req.json();
return res(
ctx.status(201),
ctx.json({ id: 3, ...newUser })
);
}),
rest.get('/api/users/:id', (req, res, ctx) => {
const { id } = req.params;
if (id === '999') {
return res(
ctx.status(404),
ctx.json({ message: 'User not found' })
);
}
return res(
ctx.json({ id: Number(id), name: `User ${id}` })
);
})
];
// setupTests.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
3. Integration Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import UserForm from '../components/UserForm';
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
});
const renderWithClient = (component) => {
const queryClient = createTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>
{component}
</QueryClientProvider>
);
};
describe('UserForm Integration', () => {
test('creates user successfully', async () => {
renderWithClient(<UserForm />);
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'New User' }
});
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'new@example.com' }
});
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(screen.getByText(/user created successfully/i)).toBeInTheDocument();
});
});
});
Popular APIs for Frontend Projects {#popular-apis}
1. Public APIs for Learning
JSONPlaceholder – Fake REST API for testing
const posts = await fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json());
OpenWeatherMap – Weather data
const weather = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=London&appid=${API_KEY}`
).then(response => response.json());
The Cat API – Random cat images
const catImage = await fetch('https://api.thecatapi.com/v1/images/search')
.then(response => response.json());
News API – Latest news articles
const news = await fetch(
`https://newsapi.org/v2/top-headlines?country=us&apiKey=${API_KEY}`
).then(response => response.json());
2. Popular Third-Party Services
Stripe API – Payment processing
import { loadStripe } from '@stripe/stripe-js';
const stripe = await loadStripe('pk_test_...');
const { error } = await stripe.redirectToCheckout({
sessionId: 'session_id_from_backend'
});
Firebase – Backend-as-a-Service
import { initializeApp } from 'firebase/app';
import { getFirestore, collection, getDocs } from 'firebase/firestore/lite';
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const querySnapshot = await getDocs(collection(db, 'users'));
const users = querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
Google Maps API – Maps and geolocation
const initMap = () => {
const map = new google.maps.Map(document.getElementById('map'), {
zoom: 4,
center: { lat: -25.344, lng: 131.036 }
});
};
Real-World Examples {#real-world-examples}
Example 1: E-commerce Product Catalog
// Product service
class ProductService {
constructor() {
this.baseURL = '/api/products';
}
async getProducts(filters = {}) {
const params = new URLSearchParams(filters);
const response = await fetch(`${this.baseURL}?${params}`);
return response.json();
}
async getProduct(id) {
const response = await fetch(`${this.baseURL}/${id}`);
return response.json();
}
async searchProducts(query) {
const response = await fetch(`${this.baseURL}/search?q=${query}`);
return response.json();
}
}
// React component
const ProductCatalog = () => {
const [products, setProducts] = useState([]);
const [filters, setFilters] = useState({
category: '',
priceMin: '',
priceMax: '',
sortBy: 'name'
});
const [loading, setLoading] = useState(true);
const productService = new ProductService();
useEffect(() => {
const loadProducts = async () => {
setLoading(true);
try {
const data = await productService.getProducts(filters);
setProducts(data.products);
} catch (error) {
console.error('Failed to load products:', error);
} finally {
setLoading(false);
}
};
loadProducts();
}, [filters]);
const handleFilterChange = (filterName, value) => {
setFilters(prev => ({
...prev,
[filterName]: value
}));
};
if (loading) return <div>Loading products...</div>;
return (
<div className="product-catalog">
<FilterPanel
filters={filters}
onChange={handleFilterChange}
/>
<ProductGrid products={products} />
</div>
);
};
Example 2: Real-time Chat Application
// WebSocket chat service
class ChatService {
constructor(userId) {
this.userId = userId;
this.socket = null;
this.messageHandlers = new Set();
}
connect() {
this.socket = new WebSocket(`ws://localhost:8080/chat?userId=${this.userId}`);
this.socket.onopen = () => {
console.log('Connected to chat server');
};
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data);
this.messageHandlers.forEach(handler => handler(message));
};
this.socket.onclose = () => {
console.log('Disconnected from chat server');
// Implement reconnection logic
setTimeout(() => this.connect(), 3000);
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
disconnect() {
if (this.socket) {
this.socket.close();
}
}
sendMessage(text, channelId) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
type: 'message',
data: {
text,
channelId,
userId: this.userId,
timestamp: new Date().toISOString()
}
}));
}
}
onMessage(handler) {
this.messageHandlers.add(handler);
return () => this.messageHandlers.delete(handler);
}
}
// React chat component
const ChatRoom = ({ channelId, userId }) => {
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState('');
const [chatService] = useState(() => new ChatService(userId));
useEffect(() => {
chatService.connect();
const unsubscribe = chatService.onMessage((message) => {
if (message.channelId === channelId) {
setMessages(prev => [...prev, message]);
}
});
return () => {
unsubscribe();
chatService.disconnect();
};
}, [chatService, channelId]);
const handleSendMessage = (e) => {
e.preventDefault();
if (newMessage.trim()) {
chatService.sendMessage(newMessage, channelId);
setNewMessage('');
}
};
return (
<div className="chat-room">
<div className="messages">
{messages.map((msg, index) => (
<div key={index} className="message">
<span className="username">{msg.username}</span>
<span className="text">{msg.text}</span>
<span className="timestamp">{msg.timestamp}</span>
</div>
))}
</div>
<form onSubmit={handleSendMessage}>
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type a message..."
/>
<button type="submit">Send</button>
</form>
</div>
);
};
Example 3: Social Media Dashboard
// Social media API service
class SocialMediaService {
constructor() {
this.baseURL = '/api/social';
}
async getUserProfile(userId) {
const response = await fetch(`${this.baseURL}/users/${userId}`);
return response.json();
}
async getFeed(userId, page = 1) {
const response = await fetch(
`${this.baseURL}/feed?userId=${userId}&page=${page}`
);
return response.json();
}
async createPost(postData) {
const response = await fetch(`${this.baseURL}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData)
});
return response.json();
}
async likePost(postId) {
const response = await fetch(`${this.baseURL}/posts/${postId}/like`, {
method: 'POST'
});
return response.json();
}
async getNotifications(userId) {
const response = await fetch(`${this.baseURL}/notifications/${userId}`);
return response.json();
}
}
// Dashboard component
const SocialDashboard = ({ userId }) => {
const [profile, setProfile] = useState(null);
const [feed, setFeed] = useState([]);
const [notifications, setNotifications] = useState([]);
const [loading, setLoading] = useState(true);
const socialService = new SocialMediaService();
useEffect(() => {
const loadDashboardData = async () => {
try {
const [profileData, feedData, notificationsData] = await Promise.all([
socialService.getUserProfile(userId),
socialService.getFeed(userId),
socialService.getNotifications(userId)
]);
setProfile(profileData);
setFeed(feedData.posts);
setNotifications(notificationsData);
} catch (error) {
console.error('Error loading dashboard:', error);
} finally {
setLoading(false);
}
};
loadDashboardData();
}, [userId]);
const handleLikePost = async (postId) => {
try {
await socialService.likePost(postId);
// Update local state
setFeed(prev => prev.map(post =>
post.id === postId
? { ...post, liked: !post.liked, likeCount: post.likeCount + (post.liked ? -1 : 1) }
: post
));
} catch (error) {
console.error('Error liking post:', error);
}
};
if (loading) return <div>Loading dashboard...</div>;
return (
<div className="social-dashboard">
<aside className="sidebar">
<UserProfile profile={profile} />
<NotificationsList notifications={notifications} />
</aside>
<main className="feed">
<PostComposer onPost={(post) => setFeed(prev => [post, ...prev])} />
<FeedList posts={feed} onLike={handleLikePost} />
</main>
</div>
);
};
Advanced Topics and Best Practices
1. API Rate Limiting and Throttling
class RateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = [];
}
async throttle(fn) {
const now = Date.now();
// Remove old requests outside the window
this.requests = this.requests.filter(
time => now - time < this.windowMs
);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = Math.min(...this.requests);
const waitTime = this.windowMs - (now - oldestRequest);
await new Promise(resolve => setTimeout(resolve, waitTime));
return this.throttle(fn);
}
this.requests.push(now);
return fn();
}
}
// Usage
const rateLimiter = new RateLimiter(100, 60000); // 100 requests per minute
const makeApiCall = (url) => rateLimiter.throttle(() => fetch(url));
2. API Versioning Strategies
class VersionedApiClient {
constructor(baseURL, version = 'v1') {
this.baseURL = baseURL;
this.version = version;
}
getEndpoint(path) {
return `${this.baseURL}/${this.version}${path}`;
}
async get(path, options = {}) {
const url = this.getEndpoint(path);
return fetch(url, {
...options,
headers: {
'Accept': `application/vnd.api+json;version=${this.version}`,
...options.headers
}
});
}
// Support for multiple versions
withVersion(version) {
return new VersionedApiClient(this.baseURL, version);
}
}
// Usage
const apiV1 = new VersionedApiClient('https://api.example.com', 'v1');
const apiV2 = apiV1.withVersion('v2');
const oldData = await apiV1.get('/users');
const newData = await apiV2.get('/users');
3. API Documentation and Types (TypeScript)
// API types
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// Typed API client
class TypedApiClient {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseURL}${endpoint}`);
return response.json();
}
async post<TRequest, TResponse>(
endpoint: string,
data: TRequest
): Promise<ApiResponse<TResponse>> {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
}
// Usage with full type safety
const api = new TypedApiClient('https://api.example.com');
const users = await api.get<User[]>('/users');
const newUser = await api.post<CreateUserRequest, User>(
'/users',
{ name: 'John', email: 'john@example.com' }
);
Troubleshooting Common API Issues
1. CORS Errors
// Problem: CORS error in browser console
// Solution 1: Proxy in development (React)
// In package.json:
{
"proxy": "http://localhost:5000"
}
// Solution 2: Use a CORS proxy service (development only)
const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
const targetUrl = 'https://api.example.com/data';
const response = await fetch(proxyUrl + targetUrl);
// Solution 3: Configure your backend properly
// Express.js example:
app.use(cors({
origin: ['http://localhost:3000', 'https://yourdomain.com'],
credentials: true
}));
2. Network Timeouts
// Problem: Requests hanging or timing out
// Solution: Implement timeout handling
const fetchWithTimeout = async (url, options = {}, timeout = 10000) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
};
3. Memory Leaks in API Calls
// Problem: Memory leaks from unresolved promises
// Solution: Proper cleanup with AbortController
const useApiWithCleanup = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, {
signal: controller.signal
});
if (!controller.signal.aborted) {
const result = await response.json();
setData(result);
}
} catch (err) {
if (!controller.signal.aborted) {
setError(err);
}
}
};
fetchData();
return () => {
controller.abort();
};
}, [url]);
return { data, error };
};
Conclusion
APIs are the backbone of modern web applications, enabling rich, dynamic user experiences by connecting frontend interfaces with backend services and third-party platforms. This comprehensive guide has covered everything from basic concepts to advanced implementation patterns.
Key Takeaways:
- Start with understanding HTTP fundamentals and REST principles
- Implement proper error handling and loading states
- Use appropriate authentication methods for your use case
- Optimize performance with caching, pagination, and request management
- Write testable code with proper mocking strategies
- Follow security best practices, especially around API keys and tokens
- Consider using libraries like React Query for advanced state management
- Always handle edge cases and network failures gracefully
Next Steps:
- Practice with public APIs to build confidence
- Implement the patterns shown in your own projects
- Explore GraphQL for more flexible data fetching
- Learn about WebSockets for real-time features
- Study API design principles to better understand backend architecture
Remember that API integration is both an art and a science. The technical implementation is important, but understanding the business context and user experience is equally crucial. Always consider performance, security, and maintainability when designing your API integration strategy.
Keep experimenting, keep learning, and don’t hesitate to refer back to this guide as you build more complex applications. The API landscape continues to evolve, but the fundamental principles covered here will serve you well throughout your frontend development journey.
