Backend
Api
API Patterns and Best Practices

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

  1. 401 Unauthorized: Automatically logs out user
  2. Network Errors: Connection issues
  3. Server Errors: 500, 502, etc.
  4. 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

  1. Centralized API Client: Single class for all API calls
  2. Automatic Authentication: Token included automatically
  3. Type Safety: TypeScript interfaces for all data
  4. Error Handling: Consistent error handling with automatic logout
  5. Pagination: Standard pagination pattern
  6. File Uploads: FormData for file uploads
  7. Loading States: Show loading during requests
  8. Debouncing: Debounce search/filter inputs
  9. Optimistic Updates: Update UI before API confirmation
  10. Error Messages: User-friendly error messages