Yaltopia-Ticket-Admin/dev-docs/API_GUIDE.md

22 KiB

API & Service Layer Guide

Complete guide for making API calls in the Yaltopia Ticket Admin application.

Table of Contents


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))
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

  1. Always use services - Never make direct API calls
  2. Use React Query - For data fetching and caching
  3. Handle errors - Provide user feedback
  4. Invalidate queries - After mutations
  5. 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: