API Patterns and Best Practices
Overview
This document outlines the patterns and best practices used for API interactions in the IBW Admin Dashboard.
API Client Architecture
Centralized API Client
All API calls go through a single ApiClient class (src/lib/api.ts):
class ApiClient {
private baseUrl: string;
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
// Centralized request handling
}
// Public methods for each resource
async getUsers(options?): Promise<PaginatedResponse<User>> { ... }
async createEvent(data): Promise<Event> { ... }
// ... more methods
}
export const apiClient = new ApiClient();Benefits
- Consistent error handling: All requests handled uniformly
- Automatic authentication: Token included automatically
- Type safety: TypeScript interfaces for all requests/responses
- Easy maintenance: Single place to update API logic
Request Patterns
GET Requests
// Simple GET
const user = await apiClient.getUser(id);
// GET with query parameters
const users = await apiClient.getUsers({
page: 1,
limit: 10,
search: "john",
roles: ["admin"]
});POST Requests
// Create resource
const newEvent = await apiClient.createEvent({
title: "Event Title",
date: "2024-01-01",
// ... other fields
});PUT Requests
// Update resource
const updated = await apiClient.updateEvent(id, {
title: "Updated Title",
// ... partial update
});DELETE Requests
// Delete resource
await apiClient.deleteEvent(id);File Upload Requests
// Image upload
const result = await apiClient.uploadImage(file);
// Excel upload
const result = await apiClient.uploadConferenceAttendees(conferenceId, file);Response Patterns
Paginated Responses
Most list endpoints return paginated data:
interface PaginatedResponse<T> {
data: T[];
meta: {
page: number;
limit: number;
total: number;
pages: number;
};
}Usage:
const response = await apiClient.getUsers({ page: 1, limit: 10 });
const users = response.users;
const { page, total, pages } = response.meta;Standard Response Format
Some endpoints use a standard response format:
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}Error Handling
Automatic Error Handling
The API client handles errors automatically:
try {
const data = await apiClient.getUsers();
} catch (error) {
// Error is already logged
// 401 errors trigger automatic logout
// Other errors throw with server message
}Error Types
- 401 Unauthorized: Automatically logs out user
- Network Errors: Connection issues
- Server Errors: 500, 502, etc.
- Validation Errors: 400 with error details
Error Messages
Server errors include messages when available:
// Server returns: { error: "Invalid input", message: "Title is required" }
// Error thrown: "HTTP error! status: 400 - Title is required"Authentication Pattern
Token Management
// Token stored in localStorage
localStorage.setItem("authToken", token);
// Automatically included in requests
headers: {
"Authorization": `Bearer ${token}`
}Automatic Logout
401 responses trigger automatic logout:
if (response.status === 401) {
forceLogout(); // Clears token and redirects to login
throw new Error("Unauthorized");
}Query Parameter Handling
Building Query Strings
The API client builds query strings from options:
const queryParams = new URLSearchParams();
if (options?.page) queryParams.append("page", options.page.toString());
if (options?.search) queryParams.append("search", options.search);
// ... more params
const url = `/api/v1/users?${queryParams.toString()}`;Array Parameters
Arrays are joined with commas:
// roles: ["admin", "user"] → "roles=admin,user"
queryParams.append("roles", roles.join(","));File Upload Patterns
FormData Upload
const formData = new FormData();
formData.append("file", file);
formData.append("data", JSON.stringify(data));
// Don't set Content-Type header - browser sets it with boundary
const response = await fetch(url, {
method: "POST",
body: formData,
headers: {
Authorization: `Bearer ${token}`
// No Content-Type header
}
});Image Upload
const result = await apiClient.uploadImage(file);
// Returns: { filename, image_url, message, size, success }
const imageUrl = result.image_url;Pagination Pattern
Client-Side Pagination
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const fetchData = async () => {
const response = await apiClient.getUsers({ page, limit });
setUsers(response.users);
setPagination(response.meta);
};Infinite Scroll (Alternative)
For large datasets, consider infinite scroll:
const [offset, setOffset] = useState(0);
const [hasMore, setHasMore] = useState(true);
const loadMore = async () => {
const response = await apiClient.getUsers({ offset, limit: 20 });
setUsers([...users, ...response.users]);
setHasMore(response.meta.has_next);
setOffset(offset + 20);
};Filtering and Search
Search Pattern
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
fetchUsers({ search: debouncedSearch });
}, [debouncedSearch]);Filter Pattern
const [filters, setFilters] = useState({
roles: [],
ecosystems: [],
topics: []
});
const applyFilters = () => {
fetchUsers({
...filters,
roles: filters.roles.join(",")
});
};Optimistic Updates
For better UX, update UI before API confirmation:
// Optimistic update
setUsers([...users, newUser]);
try {
await apiClient.createUser(newUser);
toast.success("User created");
} catch (error) {
// Rollback on error
setUsers(users);
toast.error("Failed to create user");
}Request Cancellation
For long-running requests, implement cancellation:
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, {
signal: controller.signal
});
// ... handle response
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request cancelled');
}
}
};
// Cancel request
controller.abort();Retry Logic
For failed requests, implement retry:
const retryRequest = async (fn, retries = 3) => {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
};Best Practices
1. Type Safety
Always use TypeScript interfaces:
interface User {
id: number;
name: string;
email: string;
}
const user: User = await apiClient.getUser(id);2. Error Handling
Always handle errors:
try {
const data = await apiClient.getData();
} catch (error) {
toast.error("Failed to load data");
console.error(error);
}3. Loading States
Show loading indicators:
const [loading, setLoading] = useState(false);
const fetchData = async () => {
setLoading(true);
try {
const data = await apiClient.getData();
setData(data);
} finally {
setLoading(false);
}
};4. Caching
Consider caching responses:
const cache = new Map();
const getCachedData = async (key: string) => {
if (cache.has(key)) {
return cache.get(key);
}
const data = await apiClient.getData();
cache.set(key, data);
return data;
};5. Request Debouncing
Debounce search and filter requests:
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
fetchData({ search: debouncedSearch });
}, [debouncedSearch]);Common Patterns Summary
- Centralized API Client: Single class for all API calls
- Automatic Authentication: Token included automatically
- Type Safety: TypeScript interfaces for all data
- Error Handling: Consistent error handling with automatic logout
- Pagination: Standard pagination pattern
- File Uploads: FormData for file uploads
- Loading States: Show loading during requests
- Debouncing: Debounce search/filter inputs
- Optimistic Updates: Update UI before API confirmation
- Error Messages: User-friendly error messages