mirror of https://github.com/ColorlibHQ/gentelella
29 KiB
29 KiB
layout | title | nav_order |
---|---|---|
default | API Integration | 8 |
API Integration Guide
{: .no_toc }
Learn how to integrate Gentelella Admin Template with backend APIs and external services {: .fs-6 .fw-300 }
Table of contents
{: .no_toc .text-delta }
- TOC {:toc}
REST API Integration
HTTP Client Setup
Axios Configuration
// src/js/api/http-client.js
import axios from 'axios';
class HttpClient {
constructor() {
this.client = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
this.setupInterceptors();
}
setupInterceptors() {
// Request interceptor - add auth token
this.client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle errors
this.client.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
this.handleUnauthorized();
}
return Promise.reject(this.formatError(error));
}
);
}
handleUnauthorized() {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_data');
window.location.href = '/login.html';
}
formatError(error) {
if (error.response) {
return {
message: error.response.data?.message || 'Server error',
status: error.response.status,
data: error.response.data
};
} else if (error.request) {
return {
message: 'Network error - please check your connection',
status: 0
};
} else {
return {
message: error.message || 'Unknown error occurred',
status: -1
};
}
}
// HTTP methods
get(url, config = {}) {
return this.client.get(url, config);
}
post(url, data = {}, config = {}) {
return this.client.post(url, data, config);
}
put(url, data = {}, config = {}) {
return this.client.put(url, data, config);
}
patch(url, data = {}, config = {}) {
return this.client.patch(url, data, config);
}
delete(url, config = {}) {
return this.client.delete(url, config);
}
// File upload
upload(url, file, onProgress = null) {
const formData = new FormData();
formData.append('file', file);
return this.client.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
if (onProgress) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(progress);
}
}
});
}
}
// Create singleton instance
export const httpClient = new HttpClient();
API Service Layer
Base Service Class
// src/js/api/base-service.js
import { httpClient } from './http-client.js';
export class BaseService {
constructor(endpoint) {
this.endpoint = endpoint;
this.http = httpClient;
}
async getAll(params = {}) {
try {
const response = await this.http.get(this.endpoint, { params });
return {
success: true,
data: response.data,
meta: response.meta
};
} catch (error) {
return {
success: false,
error: error.message,
details: error
};
}
}
async getById(id) {
try {
const response = await this.http.get(`${this.endpoint}/${id}`);
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message,
details: error
};
}
}
async create(data) {
try {
const response = await this.http.post(this.endpoint, data);
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message,
details: error
};
}
}
async update(id, data) {
try {
const response = await this.http.put(`${this.endpoint}/${id}`, data);
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message,
details: error
};
}
}
async delete(id) {
try {
await this.http.delete(`${this.endpoint}/${id}`);
return {
success: true
};
} catch (error) {
return {
success: false,
error: error.message,
details: error
};
}
}
async search(query, params = {}) {
try {
const response = await this.http.get(`${this.endpoint}/search`, {
params: { q: query, ...params }
});
return {
success: true,
data: response.data,
meta: response.meta
};
} catch (error) {
return {
success: false,
error: error.message,
details: error
};
}
}
}
Specific Service Classes
// src/js/api/user-service.js
import { BaseService } from './base-service.js';
class UserService extends BaseService {
constructor() {
super('/users');
}
async authenticate(credentials) {
try {
const response = await this.http.post('/auth/login', credentials);
// Store auth token
if (response.token) {
localStorage.setItem('auth_token', response.token);
localStorage.setItem('user_data', JSON.stringify(response.user));
}
return {
success: true,
data: response
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async logout() {
try {
await this.http.post('/auth/logout');
} catch (error) {
console.warn('Logout API call failed:', error.message);
} finally {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_data');
window.location.href = '/login.html';
}
}
async getCurrentUser() {
try {
const response = await this.http.get('/auth/me');
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async updateProfile(data) {
try {
const response = await this.http.put('/auth/profile', data);
// Update stored user data
localStorage.setItem('user_data', JSON.stringify(response.data));
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async changePassword(passwordData) {
try {
const response = await this.http.post('/auth/change-password', passwordData);
return {
success: true,
data: response
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async uploadAvatar(file, onProgress) {
try {
const response = await this.http.upload('/auth/avatar', file, onProgress);
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
export const userService = new UserService();
// src/js/api/dashboard-service.js
import { BaseService } from './base-service.js';
class DashboardService extends BaseService {
constructor() {
super('/dashboard');
}
async getStats(dateRange = '30d') {
try {
const response = await this.http.get('/dashboard/stats', {
params: { range: dateRange }
});
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async getChartData(chartType, params = {}) {
try {
const response = await this.http.get(`/dashboard/charts/${chartType}`, {
params
});
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async getRecentActivity(limit = 10) {
try {
const response = await this.http.get('/dashboard/activity', {
params: { limit }
});
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
export const dashboardService = new DashboardService();
Real-time Integration
WebSocket Connection
// src/js/api/websocket-client.js
class WebSocketClient {
constructor() {
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.listeners = new Map();
this.isConnected = false;
}
connect() {
const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8080/ws';
const token = localStorage.getItem('auth_token');
this.ws = new WebSocket(`${wsUrl}?token=${token}`);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
this.emit('connected');
};
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.isConnected = false;
this.emit('disconnected');
this.reconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.emit('error', error);
};
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.connect();
}, delay);
}
handleMessage(message) {
const { type, data } = message;
this.emit(type, data);
}
send(type, data = {}) {
if (!this.isConnected) {
console.warn('WebSocket not connected');
return false;
}
const message = JSON.stringify({ type, data });
this.ws.send(message);
return true;
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
off(event, callback) {
if (!this.listeners.has(event)) return;
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
emit(event, data) {
if (!this.listeners.has(event)) return;
this.listeners.get(event).forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in WebSocket event handler for ${event}:`, error);
}
});
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.isConnected = false;
}
}
// Create singleton instance
export const wsClient = new WebSocketClient();
// Auto-connect if user is authenticated
if (localStorage.getItem('auth_token')) {
wsClient.connect();
}
Real-time Dashboard Updates
// src/js/dashboard/real-time-dashboard.js
import { wsClient } from '../api/websocket-client.js';
import { dashboardService } from '../api/dashboard-service.js';
class RealTimeDashboard {
constructor() {
this.charts = new Map();
this.stats = new Map();
this.init();
}
init() {
this.setupWebSocketListeners();
this.loadInitialData();
}
setupWebSocketListeners() {
// Listen for real-time stats updates
wsClient.on('stats.update', (data) => {
this.updateStats(data);
});
// Listen for new chart data
wsClient.on('chart.data', (data) => {
this.updateChart(data.chartId, data.data);
});
// Listen for new notifications
wsClient.on('notification', (data) => {
this.showNotification(data);
});
// Listen for user activity
wsClient.on('user.activity', (data) => {
this.updateActivityFeed(data);
});
}
async loadInitialData() {
try {
// Load dashboard stats
const statsResult = await dashboardService.getStats();
if (statsResult.success) {
this.renderStats(statsResult.data);
}
// Load chart data
const chartTypes = ['sales', 'users', 'revenue'];
for (const chartType of chartTypes) {
const chartResult = await dashboardService.getChartData(chartType);
if (chartResult.success) {
this.renderChart(chartType, chartResult.data);
}
}
// Load recent activity
const activityResult = await dashboardService.getRecentActivity();
if (activityResult.success) {
this.renderActivity(activityResult.data);
}
} catch (error) {
console.error('Failed to load dashboard data:', error);
}
}
updateStats(data) {
Object.entries(data).forEach(([key, value]) => {
const element = document.querySelector(`[data-stat="${key}"]`);
if (element) {
// Animate value change
this.animateValue(element, value);
}
});
}
animateValue(element, newValue) {
const currentValue = parseFloat(element.textContent.replace(/[^0-9.-]/g, '')) || 0;
const difference = newValue - currentValue;
const steps = 30;
const stepValue = difference / steps;
let current = currentValue;
const timer = setInterval(() => {
current += stepValue;
element.textContent = this.formatValue(current, element.dataset.format);
if (--steps <= 0) {
clearInterval(timer);
element.textContent = this.formatValue(newValue, element.dataset.format);
}
}, 16);
}
formatValue(value, format) {
switch (format) {
case 'currency':
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(value);
case 'percentage':
return `${value.toFixed(1)}%`;
case 'number':
return new Intl.NumberFormat('en-US').format(Math.round(value));
default:
return value.toString();
}
}
updateChart(chartId, newData) {
const chart = this.charts.get(chartId);
if (!chart) return;
// Update chart data
chart.data = newData;
chart.update('active');
}
showNotification(data) {
// Use notification plugin or create custom notification
if (window.GentelellaPlugins && window.GentelellaPlugins.getPlugin('notifications')) {
const notifications = window.GentelellaPlugins.getPlugin('notifications');
notifications.show(data.message, data.type);
}
}
updateActivityFeed(activity) {
const feedContainer = document.querySelector('#activity-feed');
if (!feedContainer) return;
const activityItem = document.createElement('div');
activityItem.className = 'activity-item';
activityItem.innerHTML = `
<div class="activity-icon">
<i class="fa fa-${activity.icon}"></i>
</div>
<div class="activity-content">
<div class="activity-text">${activity.message}</div>
<div class="activity-time">${this.formatTime(activity.timestamp)}</div>
</div>
`;
// Add to top of feed
feedContainer.insertBefore(activityItem, feedContainer.firstChild);
// Remove oldest items if feed is too long
const items = feedContainer.querySelectorAll('.activity-item');
if (items.length > 10) {
for (let i = 10; i < items.length; i++) {
items[i].remove();
}
}
}
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString();
}
}
// Initialize real-time dashboard
new RealTimeDashboard();
Data Management
State Management
// src/js/store/app-store.js
class AppStore {
constructor() {
this.state = {
user: null,
theme: 'light',
sidebarCollapsed: false,
notifications: [],
loading: false,
error: null
};
this.listeners = new Map();
this.loadFromStorage();
}
// Get current state
getState() {
return { ...this.state };
}
// Update state
setState(updates) {
const prevState = { ...this.state };
this.state = { ...this.state, ...updates };
// Notify listeners
this.notifyListeners(prevState, this.state);
// Persist certain state to localStorage
this.saveToStorage();
}
// Subscribe to state changes
subscribe(listener) {
const id = Date.now() + Math.random();
this.listeners.set(id, listener);
// Return unsubscribe function
return () => {
this.listeners.delete(id);
};
}
notifyListeners(prevState, newState) {
this.listeners.forEach(listener => {
try {
listener(newState, prevState);
} catch (error) {
console.error('Error in state listener:', error);
}
});
}
loadFromStorage() {
try {
const userData = localStorage.getItem('user_data');
if (userData) {
this.state.user = JSON.parse(userData);
}
const theme = localStorage.getItem('theme');
if (theme) {
this.state.theme = theme;
}
const sidebarCollapsed = localStorage.getItem('sidebar-collapsed');
if (sidebarCollapsed) {
this.state.sidebarCollapsed = sidebarCollapsed === 'true';
}
} catch (error) {
console.error('Failed to load state from storage:', error);
}
}
saveToStorage() {
try {
if (this.state.user) {
localStorage.setItem('user_data', JSON.stringify(this.state.user));
}
localStorage.setItem('theme', this.state.theme);
localStorage.setItem('sidebar-collapsed', this.state.sidebarCollapsed.toString());
} catch (error) {
console.error('Failed to save state to storage:', error);
}
}
// Action methods
setUser(user) {
this.setState({ user });
}
clearUser() {
this.setState({ user: null });
localStorage.removeItem('user_data');
localStorage.removeItem('auth_token');
}
setTheme(theme) {
this.setState({ theme });
document.documentElement.setAttribute('data-theme', theme);
}
toggleSidebar() {
this.setState({ sidebarCollapsed: !this.state.sidebarCollapsed });
}
addNotification(notification) {
const notifications = [...this.state.notifications, {
id: Date.now(),
timestamp: new Date(),
...notification
}];
this.setState({ notifications });
}
removeNotification(id) {
const notifications = this.state.notifications.filter(n => n.id !== id);
this.setState({ notifications });
}
setLoading(loading) {
this.setState({ loading });
}
setError(error) {
this.setState({ error });
}
clearError() {
this.setState({ error: null });
}
}
// Create singleton instance
export const appStore = new AppStore();
// Helper hook for components
export function useStore(selector) {
const state = appStore.getState();
return selector ? selector(state) : state;
}
Data Caching
// src/js/cache/data-cache.js
class DataCache {
constructor() {
this.cache = new Map();
this.expiry = new Map();
this.defaultTTL = 5 * 60 * 1000; // 5 minutes
}
set(key, data, ttl = this.defaultTTL) {
this.cache.set(key, data);
this.expiry.set(key, Date.now() + ttl);
}
get(key) {
if (!this.cache.has(key)) {
return null;
}
const expiryTime = this.expiry.get(key);
if (Date.now() > expiryTime) {
this.delete(key);
return null;
}
return this.cache.get(key);
}
has(key) {
return this.get(key) !== null;
}
delete(key) {
this.cache.delete(key);
this.expiry.delete(key);
}
clear() {
this.cache.clear();
this.expiry.clear();
}
cleanup() {
const now = Date.now();
for (const [key, expiryTime] of this.expiry.entries()) {
if (now > expiryTime) {
this.delete(key);
}
}
}
size() {
return this.cache.size;
}
}
// Create singleton instance
export const dataCache = new DataCache();
// Auto cleanup every 5 minutes
setInterval(() => {
dataCache.cleanup();
}, 5 * 60 * 1000);
Authentication Integration
JWT Token Management
// src/js/auth/auth-manager.js
class AuthManager {
constructor() {
this.token = localStorage.getItem('auth_token');
this.refreshTimer = null;
this.init();
}
init() {
if (this.token) {
this.scheduleTokenRefresh();
}
}
async login(credentials) {
try {
const response = await userService.authenticate(credentials);
if (response.success) {
this.token = response.data.token;
this.scheduleTokenRefresh();
// Update app state
appStore.setUser(response.data.user);
return response;
}
return response;
} catch (error) {
return {
success: false,
error: error.message
};
}
}
logout() {
this.clearTokenRefresh();
this.token = null;
// Clear app state
appStore.clearUser();
// Call logout service
userService.logout();
}
isAuthenticated() {
return !!this.token && !this.isTokenExpired();
}
isTokenExpired() {
if (!this.token) return true;
try {
const payload = JSON.parse(atob(this.token.split('.')[1]));
return payload.exp * 1000 < Date.now();
} catch (error) {
return true;
}
}
async refreshToken() {
try {
const response = await httpClient.post('/auth/refresh');
if (response.token) {
this.token = response.token;
localStorage.setItem('auth_token', this.token);
this.scheduleTokenRefresh();
return true;
}
return false;
} catch (error) {
console.error('Token refresh failed:', error);
this.logout();
return false;
}
}
scheduleTokenRefresh() {
this.clearTokenRefresh();
if (!this.token) return;
try {
const payload = JSON.parse(atob(this.token.split('.')[1]));
const expiryTime = payload.exp * 1000;
const refreshTime = expiryTime - (5 * 60 * 1000); // 5 minutes before expiry
const timeUntilRefresh = refreshTime - Date.now();
if (timeUntilRefresh > 0) {
this.refreshTimer = setTimeout(() => {
this.refreshToken();
}, timeUntilRefresh);
} else {
// Token expired or will expire soon
this.refreshToken();
}
} catch (error) {
console.error('Failed to schedule token refresh:', error);
}
}
clearTokenRefresh() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
}
getToken() {
return this.token;
}
getUser() {
const userData = localStorage.getItem('user_data');
return userData ? JSON.parse(userData) : null;
}
}
// Create singleton instance
export const authManager = new AuthManager();
// Route protection
export function requireAuth() {
if (!authManager.isAuthenticated()) {
window.location.href = '/login.html';
return false;
}
return true;
}
// Auto-redirect if not authenticated (for protected pages)
if (document.querySelector('[data-require-auth]')) {
requireAuth();
}
Error Handling
Global Error Handler
// src/js/error/error-handler.js
class ErrorHandler {
constructor() {
this.setupGlobalHandlers();
}
setupGlobalHandlers() {
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
this.handleError(event.reason, 'Promise Rejection');
event.preventDefault();
});
// Handle JavaScript errors
window.addEventListener('error', (event) => {
console.error('JavaScript error:', event.error);
this.handleError(event.error, 'JavaScript Error');
});
// Handle API errors
document.addEventListener('api-error', (event) => {
this.handleApiError(event.detail);
});
}
handleError(error, context = 'Unknown') {
const errorInfo = {
message: error.message || 'Unknown error',
stack: error.stack,
context,
timestamp: new Date(),
userAgent: navigator.userAgent,
url: window.location.href,
user: authManager.getUser()?.id
};
// Log to console
console.error('Error handled:', errorInfo);
// Send to error tracking service
this.reportError(errorInfo);
// Show user-friendly notification
this.showErrorNotification(error);
}
handleApiError(error) {
if (error.status === 401) {
this.handleUnauthorized();
} else if (error.status >= 500) {
this.showErrorNotification({
message: 'Server error occurred. Please try again later.'
});
} else {
this.showErrorNotification(error);
}
}
handleUnauthorized() {
// Clear auth data and redirect to login
authManager.logout();
}
showErrorNotification(error) {
// Use notification plugin if available
if (window.GentelellaPlugins && window.GentelellaPlugins.getPlugin('notifications')) {
const notifications = window.GentelellaPlugins.getPlugin('notifications');
notifications.show(error.message || 'An error occurred', 'error');
} else {
// Fallback to alert
alert(error.message || 'An error occurred');
}
}
async reportError(errorInfo) {
try {
// Send error to monitoring service
await httpClient.post('/errors/report', errorInfo);
} catch (reportingError) {
console.error('Failed to report error:', reportingError);
}
}
}
// Initialize global error handler
new ErrorHandler();
Performance Optimization
Request Batching
// src/js/api/request-batcher.js
class RequestBatcher {
constructor() {
this.batches = new Map();
this.batchDelay = 100; // ms
}
batch(endpoint, id, params = {}) {
return new Promise((resolve, reject) => {
if (!this.batches.has(endpoint)) {
this.batches.set(endpoint, {
requests: [],
timer: null
});
}
const batch = this.batches.get(endpoint);
batch.requests.push({ id, params, resolve, reject });
// Clear existing timer and set new one
if (batch.timer) {
clearTimeout(batch.timer);
}
batch.timer = setTimeout(() => {
this.executeBatch(endpoint);
}, this.batchDelay);
});
}
async executeBatch(endpoint) {
const batch = this.batches.get(endpoint);
if (!batch || batch.requests.length === 0) return;
const requests = batch.requests.slice();
batch.requests = [];
batch.timer = null;
try {
const ids = requests.map(req => req.id);
const response = await httpClient.post(`${endpoint}/batch`, { ids });
// Resolve individual requests
requests.forEach(request => {
const result = response.data.find(item => item.id === request.id);
if (result) {
request.resolve(result);
} else {
request.reject(new Error('Item not found in batch response'));
}
});
} catch (error) {
// Reject all requests
requests.forEach(request => {
request.reject(error);
});
}
}
}
export const requestBatcher = new RequestBatcher();
Next Steps
- [Security Guide]({{ site.baseurl }}/docs/security/) - Implement security best practices
- [Testing Guide]({{ site.baseurl }}/docs/testing/) - Test your API integrations
- [Monitoring Guide]({{ site.baseurl }}/docs/monitoring/) - Monitor API performance
{: .highlight } 💡 Pro Tip: Always implement proper error handling and retry logic for API calls. Use caching strategically to reduce API load and improve user experience.