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
| Status | Meaning | Handling |
|---|---|---|
| 401 | Unauthorized | Token invalid/expired → Redirect to login |
| 403 | Forbidden | Insufficient permissions → Show error |
| 404 | Not Found | Resource doesn't exist → Show 404 page |
| 422 | Validation Error | Invalid data → Show field errors |
| 500 | Server Error | Backend issue → Show generic error |
| Network Error | No response | Offline/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
| Service | Endpoint | Purpose |
|---|---|---|
Credential | /credentials | User authentication and profile |
Module | /modules | Dynamic module management |
News | /news | News articles |
Language | /languages | Language configuration |
Stat | /stats | Statistics and metrics |
WebsiteManagement | /website-management | Site-wide settings |
File | /files | File uploads |
Address | /addresses | Physical addresses |
Contact | /contacts | Contact 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