Files
2026-02-15 11:50:39 +07:00

7.0 KiB

API Client

Complete documentation of the Axios-based API client and all available endpoints.

Overview

The API client is a centralized Axios instance with request/response interceptors for authentication and error handling.

Location: frontend/src/lib/api/

lib/api/
├── client.ts        # Axios instance with interceptors
├── index.ts         # Re-exports all API modules
├── activities.ts    # Activity log endpoints
├── auth.ts          # Authentication endpoints
├── domains.ts       # Domain management
├── favorites.ts     # User favorites
├── games.ts         # Game search/details
├── github.ts        # GitHub integration
├── jobs.ts          # Background jobs
├── platforms.ts     # Platform list
├── playTracking.ts  # Play session tracking
├── playlists.ts     # Playlist management
├── roles.ts         # Role/permission management
├── ruffle.ts        # Ruffle emulator settings
├── settings.ts      # System settings
├── sharedPlaylists.ts # Shared playlist access
├── statistics.ts    # User statistics
├── tags.ts          # Tag list
├── updates.ts       # Update checking
├── userPlaylists.ts # User playlist operations
└── users.ts         # User management

Client Configuration

const api = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json',
  },
  withCredentials: true, // Enable cookie-based auth (refresh token)
});

Important: withCredentials: true enables automatic cookie handling. The backend sets the refresh token in an HTTP-only cookie during login, and the browser automatically includes it in requests.

Interceptors

Request Interceptor

Automatically adds JWT token to all requests:

api.interceptors.request.use((config) => {
  const token = useAuthStore.getState().accessToken;
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Response Interceptor

Handles 401 errors and automatic token refresh:

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        // Refresh token is sent automatically via withCredentials (HTTP-only cookie)
        // No need to manually send refresh token
        const tokens = await authApi.refreshToken();

        useAuthStore.getState().updateAccessToken(tokens.accessToken);
        originalRequest.headers.Authorization = `Bearer ${tokens.accessToken}`;

        return api(originalRequest);
      } catch (refreshError) {
        useAuthStore.getState().clearAuth();
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

Note: The refresh token is stored in an HTTP-only cookie (sent automatically by the browser when withCredentials: true). The refreshToken() method takes no parameters and relies on the cookie being present.

API Modules

gamesApi

gamesApi.search(filters: GameFilters): Promise<PaginatedResult<Game>>
gamesApi.getFilterOptions(): Promise<FilterOptions>
gamesApi.getById(id: string): Promise<Game>
gamesApi.getRelated(id: string, limit?: number): Promise<Game[]>
gamesApi.getRandom(library?: string): Promise<Game>
gamesApi.getLaunchData(id: string): Promise<GameLaunchData>
gamesApi.downloadGame(id: string, gameDataId?: number)
gamesApi.cancelDownload(id: string)

authApi

authApi.login(credentials: LoginCredentials): Promise<LoginResponse>
authApi.register(userData: RegisterData): Promise<RegisterResponse>
authApi.logout(): Promise<void>
authApi.refreshToken(): Promise<AuthTokens>
authApi.getMe(): Promise<User>

Note: logout() and refreshToken() no longer take parameters. The refresh token is automatically sent via HTTP-only cookie when withCredentials: true is set on the axios instance.

playlistsApi

playlistsApi.getAll(): Promise<Playlist[]>
playlistsApi.getById(id: string): Promise<Playlist>
playlistsApi.create(playlist): Promise<Playlist>
playlistsApi.addGames(playlistId: string, gameIds: string[]): Promise<Playlist>
playlistsApi.removeGames(playlistId: string, gameIds: string[]): Promise<Playlist>
playlistsApi.delete(playlistId: string): Promise<void>

usersApi

usersApi.getAll(page?: number, limit?: number): Promise<UsersResponse>
usersApi.getById(id: number): Promise<UserDetails>
usersApi.create(userData: CreateUserData): Promise<UserDetails>
usersApi.update(id: number, userData: UpdateUserData): Promise<UserDetails>
usersApi.delete(id: number): Promise<void>
usersApi.changePassword(id: number, passwordData: ChangePasswordData)
usersApi.getThemeSettings(): Promise<{ mode: string; primaryColor: string }>
usersApi.updateThemeSettings(mode: string, primaryColor: string)

rolesApi

rolesApi.getAll(): Promise<Role[]>
rolesApi.getById(id: number): Promise<Role>
rolesApi.getPermissions(): Promise<Permission[]>
rolesApi.create(roleData: CreateRoleData): Promise<Role>
rolesApi.update(id: number, roleData: UpdateRoleData): Promise<Role>
rolesApi.updatePermissions(id: number, permissionIds: number[]): Promise<Role>
rolesApi.delete(id: number): Promise<void>

playTrackingApi

playTrackingApi.startSession(gameId: string, gameTitle: string): Promise<StartSessionResponse>
playTrackingApi.endSession(sessionId: string): Promise<EndSessionResponse>
playTrackingApi.getStats(): Promise<UserStats>
playTrackingApi.getGameStats(limit?: number, offset?: number)
playTrackingApi.getHistory(limit?: number, offset?: number)
playTrackingApi.getTopGames(limit?: number): Promise<GameStats[]>
playTrackingApi.getActivityOverTime(days?: number): Promise<PlayActivityData[]>
playTrackingApi.getGamesDistribution(limit?: number): Promise<GameDistribution[]>

domainsApi

domainsApi.getAll(): Promise<Domain[]>
domainsApi.add(hostname: string): Promise<Domain>
domainsApi.delete(id: number): Promise<void>
domainsApi.setDefault(id: number): Promise<Domain>

Usage with Custom Hooks

All API calls should use custom hooks wrapping TanStack Query:

// Instead of calling API directly
const games = await gamesApi.search(filters);

// Use custom hook
const { data: games, isLoading, error } = useGames(filters);

Error Handling

const { data, isLoading, error } = useGames(filters);

if (error) {
  // Handle error
  toast.error('Failed to load games');
}

Type Safety

All API responses are fully typed:

interface Game {
  id: string;
  title: string;
  platform: string;
  developer: string;
  publisher: string;
  releaseDate: string;
  // ... more fields
}

interface PaginatedResult<T> {
  games: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

Further Reading