22 KiB
22 KiB
API & Service Layer Guide
Complete guide for making API calls in the Yaltopia Ticket Admin application.
Table of Contents
- Architecture Overview
- Available Services
- Basic Usage
- Common Patterns
- Service Methods Reference
- Error Handling
- Best Practices
- Examples
Architecture Overview
The Service Layer Pattern
All API calls flow through a centralized service layer:
┌─────────────────┐
│ Component │ "I need user data"
└────────┬────────┘
│
▼
┌─────────────────┐
│ Service Layer │ userService.getUsers()
│ (Business │ • Type-safe methods
│ Logic) │ • Error handling
└────────┬────────┘ • Data transformation
│
▼
┌─────────────────┐
│ API Client │ axios.get('/admin/users')
│ (HTTP Layer) │ • Auth token injection
└────────┬────────┘ • Token refresh
│ • Request/response interceptors
▼
┌─────────────────┐
│ Backend API │ Returns JSON data
└─────────────────┘
Why Service Layer?
Before (Bad):
// Direct API calls - scattered everywhere
const response = await axios.get('/api/users')
const users = response.data
// Different patterns in different files
fetch('/api/users').then(r => r.json())
After (Good):
// Centralized, typed, consistent
import { userService } from '@/services'
const users = await userService.getUsers()
Benefits:
- ✅ Single source of truth
- ✅ Type safety (TypeScript)
- ✅ Automatic authentication
- ✅ Consistent error handling
- ✅ Easy to test
- ✅ Easy to maintain
Available Services
Import All Services
import {
authService, // Authentication & authorization
userService, // User management
analyticsService, // Dashboard analytics & metrics
securityService, // Security monitoring & logs
systemService, // System health & maintenance
announcementService,// Announcements management
auditService, // Audit logs & history
settingsService // System settings
} from '@/services'
Service Locations
src/services/
├── index.ts # Central export (import from here)
├── api/
│ └── client.ts # Shared axios instance
├── auth.service.ts # authService
├── user.service.ts # userService
├── analytics.service.ts # analyticsService
├── security.service.ts # securityService
├── system.service.ts # systemService
├── announcement.service.ts # announcementService
├── audit.service.ts # auditService
└── settings.service.ts # settingsService
Basic Usage
1. Simple API Call
import { userService } from '@/services'
// Async/await
async function loadUsers() {
const users = await userService.getUsers()
console.log(users) // Typed response
}
// Promise
userService.getUsers()
.then(users => console.log(users))
.catch(error => console.error(error))
2. With React Query (Recommended)
import { useQuery } from '@tanstack/react-query'
import { userService } from '@/services'
function UsersPage() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => userService.getUsers()
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
{data?.data.map(user => (
<div key={user.id}>{user.email}</div>
))}
</div>
)
}
3. With Parameters
import { userService } from '@/services'
// Fetch with filters
const users = await userService.getUsers({
page: 1,
limit: 20,
search: 'john',
role: 'ADMIN',
isActive: true
})
// Single user
const user = await userService.getUser('user-id-123')
4. Mutations (Create/Update/Delete)
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { userService } from '@/services'
import { toast } from 'sonner'
function DeleteUserButton({ userId }) {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: () => userService.deleteUser(userId),
onSuccess: () => {
// Refresh the users list
queryClient.invalidateQueries({ queryKey: ['users'] })
toast.success('User deleted successfully')
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to delete user')
}
})
return (
<button
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Deleting...' : 'Delete User'}
</button>
)
}
Common Patterns
Pattern 1: Fetching List Data
import { useQuery } from '@tanstack/react-query'
import { userService } from '@/services'
import { useState } from 'react'
function UsersPage() {
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
const { data, isLoading } = useQuery({
queryKey: ['users', page, search],
queryFn: () => userService.getUsers({ page, limit: 20, search })
})
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search users..."
/>
{isLoading ? (
<div>Loading...</div>
) : (
<div>
{data?.data.map(user => (
<UserCard key={user.id} user={user} />
))}
<Pagination
page={page}
totalPages={data?.totalPages}
onPageChange={setPage}
/>
</div>
)}
</div>
)
}
Pattern 2: Creating New Records
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { userService } from '@/services'
import { useState } from 'react'
function CreateUserForm() {
const queryClient = useQueryClient()
const [formData, setFormData] = useState({
email: '',
firstName: '',
lastName: '',
role: 'USER'
})
const createMutation = useMutation({
mutationFn: (data) => userService.createUser(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
toast.success('User created successfully')
setFormData({ email: '', firstName: '', lastName: '', role: 'USER' })
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to create user')
}
})
const handleSubmit = (e) => {
e.preventDefault()
createMutation.mutate(formData)
}
return (
<form onSubmit={handleSubmit}>
<input
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Email"
required
/>
{/* More fields... */}
<button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? 'Creating...' : 'Create User'}
</button>
</form>
)
}
Pattern 3: Updating Records
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { userService } from '@/services'
function UpdateUserButton({ userId, updates }) {
const queryClient = useQueryClient()
const updateMutation = useMutation({
mutationFn: () => userService.updateUser(userId, updates),
onSuccess: () => {
// Refresh both the list and the single user
queryClient.invalidateQueries({ queryKey: ['users'] })
queryClient.invalidateQueries({ queryKey: ['users', userId] })
toast.success('User updated successfully')
}
})
return (
<button onClick={() => updateMutation.mutate()}>
Update User
</button>
)
}
Pattern 4: File Upload
import { userService } from '@/services'
function ImportUsersButton() {
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
try {
const result = await userService.importUsers(file)
toast.success(`Imported ${result.imported} users. ${result.failed} failed.`)
} catch (error: any) {
toast.error(error.response?.data?.message || 'Import failed')
}
}
return (
<input
type="file"
accept=".csv"
onChange={handleFileUpload}
/>
)
}
Pattern 5: File Download
import { userService } from '@/services'
function ExportUsersButton() {
const handleExport = async () => {
try {
const blob = await userService.exportUsers('csv')
// Create download link
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `users-${new Date().toISOString()}.csv`
a.click()
window.URL.revokeObjectURL(url)
toast.success('Users exported successfully')
} catch (error: any) {
toast.error('Export failed')
}
}
return (
<button onClick={handleExport}>
Export Users
</button>
)
}
Service Methods Reference
AuthService
import { authService } from '@/services'
// Login
const response = await authService.login({
email: 'admin@example.com',
password: 'password'
})
// Returns: { accessToken, refreshToken, user }
// Logout
await authService.logout()
// Refresh token
await authService.refreshToken()
// Get current user (from localStorage)
const user = authService.getCurrentUser()
// Check if authenticated
const isAuth = authService.isAuthenticated()
// Check if admin
const isAdmin = authService.isAdmin()
UserService
import { userService } from '@/services'
// Get paginated users
const users = await userService.getUsers({
page: 1,
limit: 20,
search: 'john',
role: 'ADMIN',
isActive: true
})
// Get single user
const user = await userService.getUser('user-id')
// Create user
const newUser = await userService.createUser({
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe',
role: 'USER'
})
// Update user
const updated = await userService.updateUser('user-id', {
isActive: false,
role: 'ADMIN'
})
// Delete user
await userService.deleteUser('user-id', hard = false)
// Reset password
const result = await userService.resetPassword('user-id')
// Returns: { temporaryPassword: 'abc123' }
// Get user activity
const activity = await userService.getUserActivity('user-id', days = 30)
// Import users from CSV
const result = await userService.importUsers(file)
// Returns: { imported: 10, failed: 2 }
// Export users to CSV
const blob = await userService.exportUsers('csv')
AnalyticsService
import { analyticsService } from '@/services'
// Get overview statistics
const stats = await analyticsService.getOverview()
// Returns: { totalUsers, activeUsers, totalRevenue, ... }
// Get user growth data
const growth = await analyticsService.getUserGrowth(days = 30)
// Returns: [{ date: '2024-01-01', users: 100, ... }]
// Get revenue data
const revenue = await analyticsService.getRevenue('30days')
// Options: '7days', '30days', '90days'
// Get API usage
const apiUsage = await analyticsService.getApiUsage(days = 7)
// Get error rate
const errorRate = await analyticsService.getErrorRate(days = 7)
// Get storage analytics
const storage = await analyticsService.getStorageAnalytics()
SecurityService
import { securityService } from '@/services'
// Get suspicious activity
const suspicious = await securityService.getSuspiciousActivity()
// Returns: { suspiciousIPs: [...], suspiciousEmails: [...] }
// Get active sessions
const sessions = await securityService.getActiveSessions()
// Terminate session
await securityService.terminateSession('session-id')
// Get failed login attempts
const failedLogins = await securityService.getFailedLogins({
page: 1,
limit: 50,
email: 'user@example.com'
})
// Get rate limit violations
const violations = await securityService.getRateLimitViolations(days = 7)
// Get all API keys
const apiKeys = await securityService.getAllApiKeys()
// Revoke API key
await securityService.revokeApiKey('key-id')
// Ban IP address
await securityService.banIpAddress('192.168.1.1', 'Suspicious activity')
SystemService
import { systemService } from '@/services'
// Get system health
const health = await systemService.getHealth()
// Returns: { status: 'healthy', database: 'connected', ... }
// Get system info
const info = await systemService.getSystemInfo()
// Returns: { version, environment, memory, cpu, ... }
// Get maintenance status
const maintenance = await systemService.getMaintenanceStatus()
// Enable maintenance mode
await systemService.enableMaintenance('System upgrade in progress')
// Disable maintenance mode
await systemService.disableMaintenance()
// Clear cache
await systemService.clearCache()
// Run migrations
const result = await systemService.runMigrations()
AnnouncementService
import { announcementService } from '@/services'
// Get all announcements
const announcements = await announcementService.getAnnouncements(activeOnly = false)
// Get single announcement
const announcement = await announcementService.getAnnouncement('announcement-id')
// Create announcement
const newAnnouncement = await announcementService.createAnnouncement({
title: 'Maintenance Notice',
message: 'System will be down for maintenance',
type: 'warning',
priority: 1,
targetAudience: 'all'
})
// Update announcement
const updated = await announcementService.updateAnnouncement('announcement-id', {
title: 'Updated Title'
})
// Toggle announcement active status
await announcementService.toggleAnnouncement('announcement-id')
// Delete announcement
await announcementService.deleteAnnouncement('announcement-id')
AuditService
import { auditService } from '@/services'
// Get audit logs
const logs = await auditService.getAuditLogs({
page: 1,
limit: 50,
userId: 'user-id',
action: 'DELETE',
resourceType: 'user',
startDate: '2024-01-01',
endDate: '2024-12-31'
})
// Get audit log by ID
const log = await auditService.getAuditLog('log-id')
// Get user audit activity
const activity = await auditService.getUserAuditActivity('user-id', days = 30)
// Get resource history
const history = await auditService.getResourceHistory('user', 'resource-id')
// Get audit statistics
const stats = await auditService.getAuditStats(startDate, endDate)
// Export audit logs
const blob = await auditService.exportAuditLogs({
format: 'csv',
startDate: '2024-01-01',
endDate: '2024-12-31'
})
SettingsService
import { settingsService } from '@/services'
// Get all settings
const settings = await settingsService.getSettings(category = 'GENERAL')
// Get single setting
const setting = await settingsService.getSetting('feature_flag')
// Create setting
const newSetting = await settingsService.createSetting({
key: 'feature_flag',
value: 'true',
category: 'FEATURES',
description: 'Enable new feature',
isPublic: false
})
// Update setting
const updated = await settingsService.updateSetting('feature_flag', {
value: 'false'
})
// Delete setting
await settingsService.deleteSetting('feature_flag')
// Get public settings (for frontend use)
const publicSettings = await settingsService.getPublicSettings()
Error Handling
Standard Error Pattern
All services throw errors with consistent structure:
try {
await userService.deleteUser(userId)
toast.success('User deleted')
} catch (error: any) {
// Error structure:
// error.response.status - HTTP status code
// error.response.data.message - Error message from backend
const message = error.response?.data?.message || 'Operation failed'
toast.error(message)
}
Common HTTP Status Codes
// 400 - Bad Request (validation error)
catch (error: any) {
if (error.response?.status === 400) {
toast.error('Invalid input: ' + error.response.data.message)
}
}
// 401 - Unauthorized (handled automatically by interceptor)
// Token refresh attempted automatically
// If refresh fails, user redirected to login
// 403 - Forbidden (no permission)
catch (error: any) {
if (error.response?.status === 403) {
toast.error('You do not have permission to perform this action')
}
}
// 404 - Not Found
catch (error: any) {
if (error.response?.status === 404) {
toast.error('Resource not found')
}
}
// 500 - Server Error
catch (error: any) {
if (error.response?.status === 500) {
toast.error('Server error. Please try again later.')
}
}
React Query Error Handling
const { data, error } = useQuery({
queryKey: ['users'],
queryFn: () => userService.getUsers(),
retry: 3, // Retry failed requests
retryDelay: 1000, // Wait 1s between retries
})
// Display error
if (error) {
return <div>Error: {error.message}</div>
}
Best Practices
✅ DO: Use Services
// Good
import { userService } from '@/services'
const users = await userService.getUsers()
❌ DON'T: Direct API Calls
// Bad - don't do this
import axios from 'axios'
const response = await axios.get('/api/users')
✅ DO: Use React Query
// Good - caching, loading states, error handling
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => userService.getUsers()
})
❌ DON'T: Manual State Management
// Bad - manual state management
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
userService.getUsers()
.then(setUsers)
.finally(() => setLoading(false))
}, [])
✅ DO: Specific Query Keys
// Good - specific, cacheable
queryKey: ['users', page, limit, search]
❌ DON'T: Generic Query Keys
// Bad - too generic
queryKey: ['data']
✅ DO: Invalidate After Mutations
// Good - refresh data after changes
const mutation = useMutation({
mutationFn: userService.deleteUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
}
})
✅ DO: Handle Errors
// Good - user feedback
try {
await userService.deleteUser(id)
toast.success('User deleted')
} catch (error: any) {
toast.error(error.response?.data?.message)
}
❌ DON'T: Ignore Errors
// Bad - no error handling
await userService.deleteUser(id)
Examples
Complete CRUD Example
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { userService } from '@/services'
import { toast } from 'sonner'
import { useState } from 'react'
function UsersManagement() {
const queryClient = useQueryClient()
const [page, setPage] = useState(1)
const [editingUser, setEditingUser] = useState(null)
// READ - Fetch users
const { data: users, isLoading } = useQuery({
queryKey: ['users', page],
queryFn: () => userService.getUsers({ page, limit: 20 })
})
// CREATE - Add new user
const createMutation = useMutation({
mutationFn: (data) => userService.createUser(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
toast.success('User created')
}
})
// UPDATE - Edit user
const updateMutation = useMutation({
mutationFn: ({ id, data }) => userService.updateUser(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
setEditingUser(null)
toast.success('User updated')
}
})
// DELETE - Remove user
const deleteMutation = useMutation({
mutationFn: (id) => userService.deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
toast.success('User deleted')
}
})
if (isLoading) return <div>Loading...</div>
return (
<div>
<h1>Users Management</h1>
{/* User List */}
<table>
<thead>
<tr>
<th>Email</th>
<th>Name</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users?.data.map(user => (
<tr key={user.id}>
<td>{user.email}</td>
<td>{user.firstName} {user.lastName}</td>
<td>{user.role}</td>
<td>
<button onClick={() => setEditingUser(user)}>
Edit
</button>
<button onClick={() => deleteMutation.mutate(user.id)}>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
<div>
<button
onClick={() => setPage(p => p - 1)}
disabled={page === 1}
>
Previous
</button>
<span>Page {page} of {users?.totalPages}</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={page === users?.totalPages}
>
Next
</button>
</div>
</div>
)
}
Summary
Key Takeaways
- Always use services - Never make direct API calls
- Use React Query - For data fetching and caching
- Handle errors - Provide user feedback
- Invalidate queries - After mutations
- Use specific query keys - For better caching
Quick Reference
// Import
import { userService } from '@/services'
// Fetch
const { data } = useQuery({
queryKey: ['users'],
queryFn: () => userService.getUsers()
})
// Mutate
const mutation = useMutation({
mutationFn: (id) => userService.deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
toast.success('Success')
}
})
// Error handling
try {
await userService.deleteUser(id)
} catch (error: any) {
toast.error(error.response?.data?.message)
}
For more information:
- Development Guide - General development practices
- Testing Guide - How to test services
- Security Guide - Security best practices