Aller au contenu principal

API Architecture Documentation

Overview

The Baldr Dashboard uses a class-based API architecture with a centralized base class (BaseAPI) that all API services extend. This pattern provides consistent authentication, error handling, and HTTP operations across the entire application.

Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│ Frontend │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ News API │ │ Events API │ │ Modules API │ │
│ │ │ │ │ │ │ │
│ │ endpoint: │ │ endpoint: │ │ endpoint: │ │
│ │ "/news" │ │ "/events" │ │ "/modules" │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────────────────────┴────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ BaseAPI │ │
│ │ │ │
│ │ - Axios Instance│ │
│ │ - Interceptors │ │
│ │ - HTTP Methods │ │
│ │ - Auth Token │ │
│ └────────┬────────┘ │
│ │ │
├──────────────────────────────┼──────────────────────────────┤
│ │ │
│ ┌────────▼────────┐ │
│ │ Axios (HTTP) │ │
│ └────────┬────────┘ │
│ │ │
└──────────────────────────────┼──────────────────────────────┘

│ Bearer Token

┌──────────────────────────────▼──────────────────────────────┐
│ Backend API │
│ (VITE_API_URL) │
└─────────────────────────────────────────────────────────────┘

Core Components

1. BaseAPI Class

Location: app/api/base.api.ts

Responsibilities:

  • Axios instance initialization and configuration
  • Request/response interceptors
  • Authentication token management
  • HTTP method wrappers (GET, POST, PUT, DELETE, PATCH)
  • URL building and endpoint management
  • Centralized error handling

Key Features:

a) Authentication

// Automatic token injection
this.axiosInstance.interceptors.request.use((config) => {
const token = Cookies.get("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

b) Error Handling

// Consistent error transformation
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
const { status, data } = error.response;
throw new ApiException(data?.message, status, data?.code);
}
throw new ApiException(`Network error: ${error.message}`);
}
);

c) HTTP Methods

protected async get<T>(path: string = ""): Promise<T>
protected async post<T>(path: string = "", data?: any, requiresAuth?: boolean): Promise<T>
protected async put<T>(path: string = "", data?: any): Promise<T>
protected async delete<T>(path: string = "", data?: any): Promise<T>
protected async patch<T>(path: string = "", data?: any): Promise<T>

2. API Service Classes

Each module/feature has its own API service class that extends BaseAPI.

Example Structure:

// app/api/news.api.ts
import { BaseAPI } from "./base.api";
import type { INews } from "~/interfaces/news.interface";

class NewsAPI extends BaseAPI {
protected endpoint = "/news";

/**
* Fetch all news articles
*/
async getAll(params?: any) {
return this.get<{ items: INews[] }>("", { params });
}

/**
* Fetch single news article by ID
*/
async getById(id: string) {
return this.get<INews>(id);
}

/**
* Create new news article
*/
async create(data: Partial<INews>) {
return this.post<INews>("", data);
}

/**
* Update existing news article
*/
async update(id: string, data: Partial<INews>) {
return this.put<INews>(id, data);
}

/**
* Delete news article
*/
async delete(id: string) {
return this.delete<void>(id);
}

/**
* Search news articles
*/
async search(params: any) {
return this.post<{ items: INews[] }>("/search", params);
}
}

// Export singleton instance
export default new NewsAPI();

Standard API Patterns

Pattern 1: CRUD Operations

Most API services implement standard CRUD operations:

class ModuleAPI extends BaseAPI {
protected endpoint = "/your-module";

// CREATE
async create(data: T) {
return this.post<T>("", data);
}

// READ (all)
async getAll(filters?: any) {
return this.get<{ items: T[] }>("", { params: filters });
}

// READ (single)
async getById(id: string) {
return this.get<T>(id);
}

// UPDATE
async update(id: string, data: Partial<T>) {
return this.put<T>(id, data);
}

// DELETE
async delete(id: string) {
return this.delete<void>(id);
}
}

Pattern 2: Search/Filter

Complex queries use POST for search:

async search(params: {
module_id: string;
search?: any[];
outputFields?: string[];
sort?: any;
limit?: number;
skip?: number;
}) {
return this.post<{ items: T[] }>("/search", params);
}

Pattern 3: Bulk Operations

Bulk actions pass data array in request body:

async bulkDelete(ids: string[]) {
return this.delete<{ deletedCount: number }>("/bulk", { ids });
}

async bulkUpdate(updates: Array<{ id: string; data: Partial<T> }>) {
return this.post<{ updatedCount: number }>("/bulk-update", updates);
}

Pattern 4: Public Endpoints

Authentication bypass for login/register:

async login(email: string, password: string) {
// Third parameter = false disables auth requirement
return this.post<{ token: string; user: User }>(
"/login",
{ email, password },
false // requiresAuth = false
);
}

URL Building Logic

Endpoint + Path Combination

protected buildUrl(path: string = ""): string {
// Cleans and combines endpoint with path
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
const cleanEndpoint = this.endpoint.startsWith("/")
? this.endpoint.slice(1)
: this.endpoint;
return `/${cleanEndpoint}${cleanPath ? `/${cleanPath}` : ""}`;
}

Examples:

// In NewsAPI (endpoint = "/news")
this.buildUrl("") // "/news"
this.buildUrl("123") // "/news/123"
this.buildUrl("123/edit") // "/news/123/edit"
this.buildUrl("/search") // "/news/search" (removes leading /)

Error Handling

ApiException Class

Custom error class with additional context:

export class ApiException extends Error {
public status?: number; // HTTP status code
public code?: string; // Backend error code

constructor(message: string, status?: number, code?: string) {
super(message);
this.name = "ApiException";
this.status = status;
this.code = code;
}
}

Common Error Scenarios

StatusMeaningHandling
401UnauthorizedToken invalid/expired → Redirect to login
403ForbiddenInsufficient permissions → Show error
404Not FoundResource doesn't exist → Show 404 page
422Validation ErrorInvalid data → Show field errors
500Server ErrorBackend issue → Show generic error
Network ErrorNo responseOffline/timeout → Show connection error

Usage in Components

try {
const news = await News.getById(id);
setData(news);
} catch (error) {
if (error instanceof ApiException) {
if (error.status === 404) {
navigate("/not-found");
} else if (error.status === 401) {
navigate("/login");
} else {
toast.error(error.message);
}
}
}

Integration with TanStack Query

API services are designed to work seamlessly with TanStack Query:

import News from "~/api/news.api";

// In component
const { data, isLoading, error } = useQuery({
queryKey: ["news", newsId],
queryFn: () => News.getById(newsId),
});

// Mutations
const mutation = useMutation({
mutationFn: (data: NewsData) => News.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["news"] });
},
});

Authentication Flow

1. Login Process

User Login

POST /oauth/login (requiresAuth: false)

Backend validates credentials

Returns { token, user }

Frontend stores token in cookie

Future requests include token automatically

2. Token Management

// Token stored in cookie
Cookies.set("token", authToken);

// Automatic inclusion in requests
config.headers.Authorization = `Bearer ${token}`;

// Token check
protected isAuthenticated(): boolean {
return !!Cookies.get("token");
}

3. Logout Process

const logout = () => {
Cookies.remove("token");
navigate("/connexion");
};

Best Practices

1. Creating New API Services

// Step 1: Create interface
// app/interfaces/product.interface.ts
export interface IProduct {
_id: string;
name: string;
price: number;
active: boolean;
}

// Step 2: Create API service
// app/api/product.api.ts
import { BaseAPI } from "./base.api";
import type { IProduct } from "~/interfaces/product.interface";

class ProductAPI extends BaseAPI {
protected endpoint = "/products";

async getAll() {
return this.get<{ items: IProduct[] }>();
}

async getById(id: string) {
return this.get<IProduct>(id);
}

async create(data: Partial<IProduct>) {
return this.post<IProduct>("", data);
}

async update(id: string, data: Partial<IProduct>) {
return this.put<IProduct>(id, data);
}

async delete(id: string) {
return this.delete<void>(id);
}
}

export default new ProductAPI();

// Step 3: Use in components
import Product from "~/api/product.api";

const { data } = useQuery({
queryKey: ["products"],
queryFn: () => Product.getAll(),
});

2. Type Safety

Always use TypeScript generics for type-safe responses:

// ✅ Good - Typed response
const news = await this.get<INews>(id);

// ❌ Bad - Untyped response
const news = await this.get(id);

3. Error Handling

Always handle errors in components:

// ✅ Good
try {
await News.create(data);
toast.success("Created!");
} catch (error) {
if (error instanceof ApiException) {
toast.error(error.message);
}
}

// ❌ Bad - No error handling
await News.create(data);

4. TanStack Query Integration

Use TanStack Query for data fetching:

// ✅ Good - Automatic caching, refetching, loading states
const { data, isLoading } = useQuery({
queryKey: ["news", id],
queryFn: () => News.getById(id),
});

// ❌ Bad - Manual state management
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
News.getById(id).then(setData).finally(() => setLoading(false));
}, [id]);

Configuration

Environment Variables

# .env
VITE_API_URL=https://api.example.com

Timeout Configuration

Default timeout: 30 seconds

this.axiosInstance = axios.create({
baseURL: baseUrl,
timeout: 30000, // 30 seconds
});

Common API Services

ServiceEndpointPurpose
Credential/credentialsUser authentication and profile
Module/modulesDynamic module management
News/newsNews articles
Language/languagesLanguage configuration
Stat/statsStatistics and metrics
WebsiteManagement/website-managementSite-wide settings
File/filesFile uploads
Address/addressesPhysical addresses
Contact/contactsContact form submissions

Testing API Services

// Mock for testing
jest.mock("~/api/news.api", () => ({
default: {
getAll: jest.fn(),
getById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
}));

// Test
it("should fetch news", async () => {
const mockNews = [{ _id: "1", title: "Test" }];
News.getAll.mockResolvedValue({ items: mockNews });

const result = await News.getAll();
expect(result.items).toEqual(mockNews);
});

Troubleshooting

Issue: 401 Unauthorized

Cause: Token expired or invalid
Solution: Check if token exists in cookies, redirect to login if missing

Issue: Network Error

Cause: API server down, CORS issue, or timeout
Solution: Check VITE_API_URL, verify backend is running, check network tab

Issue: Type Errors

Cause: API response doesn't match interface
Solution: Update interface to match backend response structure


Last Updated: October 28, 2025
Version: 1.0.0