workspace/projects/gamilit/docs/95-guias-desarrollo/frontend/API-INTEGRATION.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- Configure workspace Git repository with comprehensive .gitignore
- Add Odoo as submodule for ERP reference code
- Include documentation: SETUP.md, GIT-STRUCTURE.md
- Add gitignore templates for projects (backend, frontend, database)
- Structure supports independent repos per project/subproject level

Workspace includes:
- core/ - Reusable patterns, modules, orchestration system
- projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.)
- knowledge-base/ - Reference code and patterns (includes Odoo submodule)
- devtools/ - Development tools and templates
- customers/ - Client implementations template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 10:44:23 -06:00

11 KiB

Integración con API Backend

Versión: 1.0.0 Última Actualización: 2025-11-28 Aplica a: apps/frontend/src/


Resumen

Este documento describe cómo el frontend se comunica con el backend de GAMILIT, incluyendo configuración de Axios, manejo de autenticación, y patrones de servicios.


Configuración de Axios

Instancia Base

// shared/lib/axios.ts
import axios from 'axios';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1';

export const api = axios.create({
  baseURL: API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
  timeout: 30000, // 30 segundos
});

Interceptor de Autenticación

// shared/lib/axios.ts
import { useAuthStore } from '@/features/auth/stores/auth.store';

// Request: Añadir token
api.interceptors.request.use(
  (config) => {
    const token = useAuthStore.getState().token;
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response: Manejar errores
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      useAuthStore.getState().logout();
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

Estructura de Servicios

Servicio Básico

// features/gamification/services/gamification.service.ts
import { api } from '@/shared/lib/axios';
import type { UserStats, Achievement, LeaderboardEntry } from '../types';
import type { PaginatedResponse } from '@/shared/types/api.types';

class GamificationService {
  private readonly basePath = '/gamification';

  async getMyStats(): Promise<UserStats> {
    const { data } = await api.get<UserStats>(`${this.basePath}/stats`);
    return data;
  }

  async getAchievements(): Promise<Achievement[]> {
    const { data } = await api.get<Achievement[]>(`${this.basePath}/achievements`);
    return data;
  }

  async getLeaderboard(
    params: LeaderboardParams
  ): Promise<PaginatedResponse<LeaderboardEntry>> {
    const { data } = await api.get<PaginatedResponse<LeaderboardEntry>>(
      `${this.basePath}/leaderboard`,
      { params }
    );
    return data;
  }

  async purchaseComodin(comodinId: string): Promise<void> {
    await api.post(`${this.basePath}/comodines/purchase`, { comodinId });
  }

  async useComodin(comodinId: string, exerciseId: string): Promise<void> {
    await api.post(`${this.basePath}/comodines/use`, { comodinId, exerciseId });
  }
}

export const gamificationService = new GamificationService();

Servicio con CRUD Completo

// features/exercises/services/exercises.service.ts
import { api } from '@/shared/lib/axios';
import type { Exercise, ExerciseFilters, SubmissionResult } from '../types';

class ExercisesService {
  private readonly basePath = '/educational/exercises';

  async getAll(filters: ExerciseFilters): Promise<PaginatedResponse<Exercise>> {
    const { data } = await api.get(this.basePath, { params: filters });
    return data;
  }

  async getById(id: string): Promise<Exercise> {
    const { data } = await api.get<Exercise>(`${this.basePath}/${id}`);
    return data;
  }

  async submitAnswer(exerciseId: string, answer: unknown): Promise<SubmissionResult> {
    const { data } = await api.post<SubmissionResult>(
      `${this.basePath}/${exerciseId}/submit`,
      { answer }
    );
    return data;
  }
}

export const exercisesService = new ExercisesService();

Tipos de Respuesta

Tipos Comunes

// shared/types/api.types.ts

// Respuesta paginada
export interface PaginatedResponse<T> {
  data: T[];
  meta: {
    total: number;
    page: number;
    limit: number;
    totalPages: number;
  };
}

// Error de API
export interface ApiError {
  statusCode: number;
  code: string;
  message: string;
  errors?: Array<{
    field: string;
    messages: string[];
  }>;
}

// Params de paginación
export interface PaginationParams {
  page?: number;
  limit?: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}

Tipos de Feature

// features/gamification/types/user-stats.types.ts
export interface UserStats {
  id: string;
  userId: string;
  totalXp: number;
  currentLevel: number;
  mlCoins: number;
  currentStreak: number;
  longestStreak: number;
  globalRank?: number;
}

// features/gamification/types/achievement.types.ts
export interface Achievement {
  id: string;
  name: string;
  description: string;
  iconUrl: string;
  xpReward: number;
  coinsReward: number;
  category: AchievementCategory;
  isSecret: boolean;
  conditions: Record<string, unknown>;
}

export interface UserAchievement {
  id: string;
  achievementId: string;
  achievement: Achievement;
  progress: number;
  isCompleted: boolean;
  completedAt?: string;
  isClaimed: boolean;
}

Integración con React Query

Query Hooks

// features/gamification/hooks/useUserStats.ts
import { useQuery } from '@tanstack/react-query';
import { gamificationService } from '../services/gamification.service';

export const useUserStats = () => {
  return useQuery({
    queryKey: ['user-stats'],
    queryFn: () => gamificationService.getMyStats(),
    staleTime: 2 * 60 * 1000, // 2 minutos
  });
};

// features/gamification/hooks/useAchievements.ts
export const useAchievements = () => {
  return useQuery({
    queryKey: ['achievements'],
    queryFn: () => gamificationService.getAchievements(),
  });
};

// features/gamification/hooks/useLeaderboard.ts
export const useLeaderboard = (filters: LeaderboardFilters) => {
  return useQuery({
    queryKey: ['leaderboard', filters],
    queryFn: () => gamificationService.getLeaderboard(filters),
    placeholderData: keepPreviousData, // Mantener datos mientras carga
  });
};

Mutation Hooks

// features/gamification/hooks/usePurchaseComodin.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { gamificationService } from '../services/gamification.service';
import { toast } from 'sonner';

export const usePurchaseComodin = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (comodinId: string) =>
      gamificationService.purchaseComodin(comodinId),

    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['user-stats'] });
      queryClient.invalidateQueries({ queryKey: ['comodines-inventory'] });
      toast.success('Comodín comprado exitosamente');
    },

    onError: (error: AxiosError<ApiError>) => {
      const message = error.response?.data?.message || 'Error al comprar comodín';
      toast.error(message);
    },
  });
};

Manejo de Errores

Error Handler Global

// shared/lib/axios.ts
import { toast } from 'sonner';

api.interceptors.response.use(
  (response) => response,
  (error) => {
    const status = error.response?.status;
    const message = error.response?.data?.message;

    switch (status) {
      case 400:
        // Validation errors - handled locally
        break;
      case 401:
        useAuthStore.getState().logout();
        window.location.href = '/login';
        break;
      case 403:
        toast.error('No tienes permiso para realizar esta acción');
        break;
      case 404:
        // Usually handled locally
        break;
      case 500:
        toast.error('Error del servidor. Intenta de nuevo más tarde.');
        break;
      default:
        toast.error(message || 'Ocurrió un error inesperado');
    }

    return Promise.reject(error);
  }
);

Error en Componente

const ExerciseForm = () => {
  const { mutate: submit, error, isPending } = useSubmitAnswer();

  return (
    <form onSubmit={handleSubmit}>
      {error && (
        <div className="text-red-500">
          {error.response?.data?.message || 'Error al enviar'}
        </div>
      )}
      <Button type="submit" disabled={isPending}>
        Enviar
      </Button>
    </form>
  );
};

WebSocket Integration

Configuración

// features/notifications/hooks/useWebSocket.ts
import { useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
import { useAuthStore } from '@/features/auth';

let socket: Socket | null = null;

export const useWebSocket = () => {
  const token = useAuthStore((s) => s.token);
  const queryClient = useQueryClient();

  useEffect(() => {
    if (!token) return;

    socket = io(import.meta.env.VITE_WS_URL || 'http://localhost:3000', {
      auth: { token },
    });

    socket.on('notification', (notification) => {
      queryClient.invalidateQueries({ queryKey: ['notifications'] });
      toast.info(notification.message);
    });

    socket.on('achievement_unlocked', (achievement) => {
      queryClient.invalidateQueries({ queryKey: ['user-stats'] });
      queryClient.invalidateQueries({ queryKey: ['achievements'] });
      showAchievementModal(achievement);
    });

    socket.on('xp_gained', (data) => {
      queryClient.invalidateQueries({ queryKey: ['user-stats'] });
    });

    return () => {
      socket?.disconnect();
    };
  }, [token, queryClient]);

  return socket;
};

Endpoints Principales

Auth

Método Endpoint Descripción
POST /auth/login Iniciar sesión
POST /auth/register Registrar usuario
POST /auth/logout Cerrar sesión
POST /auth/refresh Refrescar token
GET /auth/me Usuario actual

Gamification

Método Endpoint Descripción
GET /gamification/stats Stats del usuario
GET /gamification/achievements Logros disponibles
GET /gamification/achievements/user Logros del usuario
GET /gamification/leaderboard Tabla de posiciones
GET /gamification/ranks Rangos Maya
POST /gamification/comodines/purchase Comprar comodín
POST /gamification/comodines/use Usar comodín

Educational

Método Endpoint Descripción
GET /educational/modules Módulos educativos
GET /educational/exercises Ejercicios
GET /educational/exercises/:id Detalle de ejercicio
POST /educational/exercises/:id/submit Enviar respuesta

Progress

Método Endpoint Descripción
GET /progress/module/:id Progreso por módulo
GET /progress/submissions Entregas del usuario
GET /progress/sessions Sesiones de aprendizaje

Buenas Prácticas

  1. Un servicio por dominio: Agrupar endpoints relacionados
  2. Tipos para todo: Request y response tipados
  3. Query keys consistentes: ['resource', id, 'sub-resource']
  4. Manejar loading/error: En cada componente
  5. Invalidar queries: Después de mutaciones exitosas
  6. Mensajes de error claros: Toast con mensaje específico

Ver También