- 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>
9.6 KiB
ADR-011: Estructura de API Clients en Frontend
Estado: Aceptado Fecha: 2025-11-23 Autor: Architecture-Analyst Relacionado con: BUG-FRONTEND-001, BUG-FRONTEND-002, Frontend API Architecture
Contexto
El frontend de GAMILIT necesita comunicarse con el backend a través de APIs REST. Hemos identificado varios problemas relacionados con la estructura y uso de API clients:
- BUG-FRONTEND-001: Imports rotos causaron caída completa del frontend
- BUG-FRONTEND-002: Rutas hard-coded con
/v1/incorrectas causaron errores 404 - Inconsistencia: Diferentes formas de llamar a las mismas APIs (hooks vs API modules)
- Falta de documentación: No existía guía clara sobre cómo estructurar y usar API clients
Problemas Identificados
- Hard-coding de rutas en hooks (anti-patrón)
- Rutas duplicadas y inconsistentes
- Imports rotos por refactorizaciones incompletas
- No hay single source of truth para rutas
Decisión
Adoptamos la siguiente estructura de API clients para el frontend:
1. Cliente Base Axios (services/api/apiClient.ts)
Responsabilidad: Instancia base de Axios con configuración global
Ubicación: apps/frontend/src/services/api/apiClient.ts
Contenido:
- Instancia de Axios configurada con baseURL, timeout, headers
- Interceptors de request (auth token, tenant-id)
- Interceptors de response (refresh token, manejo de errores)
- Funciones utilitarias (setAuthToken, clearAuthTokens, isAuthenticated)
Export: export default apiClient
Ejemplo:
// apps/frontend/src/services/api/apiClient.ts
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3006/api';
export const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Interceptors...
export default apiClient;
2. Módulos API Específicos (lib/api/*.api.ts)
Responsabilidad: Definir métodos de API por dominio/módulo
Ubicación: apps/frontend/src/lib/api/
Estructura:
apps/frontend/src/lib/api/
├── auth.api.ts # Autenticación (login, register, logout, profile)
├── gamification.api.ts # Gamificación (stats, achievements, leaderboard, ML coins)
├── progress.api.ts # Progreso (módulos, sesiones, intentos, actividades)
├── educational.api.ts # Contenido educativo (módulos, ejercicios, búsqueda)
└── index.ts # Re-exportación de todos los APIs
Patrón de implementación:
// apps/frontend/src/lib/api/gamification.api.ts
import apiClient from '@/services/api/apiClient';
import type { UserStats, Achievement } from '@/shared/types';
export const gamificationApi = {
getUserStats: async (userId: string): Promise<UserStats> => {
const { data } = await apiClient.get<UserStats>(`/gamification/users/${userId}/stats`);
return data;
},
getUserAchievements: async (userId: string): Promise<Achievement[]> => {
const { data } = await apiClient.get<Achievement[]>(`/gamification/users/${userId}/achievements`);
return data;
},
};
export default gamificationApi;
3. Uso en Hooks y Componentes
Regla: Hooks y componentes DEBEN usar módulos API, NO hacer llamadas directas con apiClient.
✅ CORRECTO:
// apps/frontend/src/hooks/useUserGamification.ts
import { gamificationApi } from '@/lib/api/gamification.api';
export function useUserGamification(userId: string) {
const fetchData = async () => {
const [stats, achievements] = await Promise.all([
gamificationApi.getUserStats(userId), // ✅ Usa API module
gamificationApi.getUserAchievements(userId) // ✅ Usa API module
]);
};
}
❌ INCORRECTO:
// ❌ Hard-coding de rutas en hooks
import { apiClient } from '@/services/api/apiClient';
export function useUserGamification(userId: string) {
const fetchData = async () => {
const statsResponse = await apiClient.get(`/gamification/users/${userId}/stats`); // ❌
const achievementsResponse = await apiClient.get(`/gamification/users/${userId}/achievements`); // ❌
};
}
4. Estructura de Rutas
Regla: Las rutas deben coincidir EXACTAMENTE con las expuestas por el backend.
Backend:
// apps/backend/src/main.ts
app.setGlobalPrefix('api'); // Global prefix: /api
// apps/backend/src/modules/gamification/controllers/user-stats.controller.ts
@Controller('gamification') // Controller base: gamification
@Get('users/:userId/stats') // Route: users/:userId/stats
// Ruta final: /api/gamification/users/:userId/stats
Frontend:
// apps/frontend/src/services/api/apiClient.ts
const API_BASE_URL = 'http://localhost:3006/api'; // baseURL incluye /api
// apps/frontend/src/lib/api/gamification.api.ts
apiClient.get(`/gamification/users/${userId}/stats`)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Ruta sin /api (ya está en baseURL)
// Ruta final: http://localhost:3006/api/gamification/users/:userId/stats ✅
⚠️ IMPORTANTE: NO agregar /v1/ a las rutas a menos que el backend lo tenga en el global prefix.
Consecuencias
Positivas ✅
-
Single Source of Truth
- Rutas definidas una sola vez en módulos
*.api.ts - Fácil de actualizar si backend cambia rutas
- Rutas definidas una sola vez en módulos
-
Type Safety
- TypeScript types en todos los API methods
- Intellisense para parameters y return types
-
Mantenibilidad
- Cambios en rutas se hacen en un solo lugar
- Hooks usan métodos typed, no strings hardcoded
-
Testability
- Módulos API pueden mockearse fácilmente
- Tests unitarios más simples
-
Prevención de Bugs
- Imports claros y centralizados
- No más hard-coding de rutas
- Refactorings más seguros
Negativas ⚠️
-
Capa Adicional
- Una capa extra entre hooks y Axios
- Pequeño overhead de abstracción
-
Duplicación de Types
- Types deben definirse tanto en frontend como backend
- Requiere sincronización manual
Mitigaciones 🛡️
-
Para duplicación de types:
- Usar generadores automáticos de tipos (futuro: OpenAPI/Swagger)
- Documentar contratos en shared types
-
Para sincronización frontend-backend:
- Implementar validación automática de contratos en CI/CD (futuro)
- Documentar rutas en
routes.constants.tscomo referencia
Alternativas Consideradas
Alternativa 1: Usar directamente apiClient en hooks
Descartada porque:
- Hard-coding de rutas en múltiples lugares
- Propenso a errores (como vimos en BUG-FRONTEND-002)
- Difícil de mantener
Alternativa 2: Usar librería como RTK Query o React Query
Descartada porque:
- Overhead de aprendizaje del equipo
- Refactorización masiva del código existente
- No resuelve el problema de hard-coding de rutas
Alternativa 3: Generación automática desde OpenAPI/Swagger
Considerada para futuro:
- Requiere configurar OpenAPI en backend (no existe actualmente)
- Puede implementarse como mejora futura sin romper esta arquitectura
- Compatible con la estructura actual
Guía de Implementación
Para Crear un Nuevo Módulo API
-
Crear archivo
*.api.tsenlib/api/// apps/frontend/src/lib/api/nuevo-modulo.api.ts import apiClient from '@/services/api/apiClient'; import type { TypeA, TypeB } from '@/shared/types'; export const nuevoModuloApi = { getItem: async (id: string): Promise<TypeA> => { const { data } = await apiClient.get<TypeA>(`/nuevo-modulo/items/${id}`); return data; }, createItem: async (payload: TypeB): Promise<TypeA> => { const { data } = await apiClient.post<TypeA>(`/nuevo-modulo/items`, payload); return data; }, }; export default nuevoModuloApi; -
Agregar export en
lib/api/index.tsexport * from './nuevo-modulo.api'; -
Usar en hooks/componentes
import { nuevoModuloApi } from '@/lib/api'; const item = await nuevoModuloApi.getItem(id);
Para Refactorizar un Hook Existente
Antes (hard-coded routes):
import { apiClient } from '@/services/api/apiClient';
const response = await apiClient.get(`/gamification/users/${userId}/stats`);
const stats = response.data;
Después (usa API module):
import { gamificationApi } from '@/lib/api/gamification.api';
const stats = await gamificationApi.getUserStats(userId);
Validación
Esta decisión se valida mediante:
- Code reviews: Verificar que nuevos PRs siguen esta estructura
- ESLint rules (futuro): Regla custom para detectar hard-coding de rutas
- Documentación: Este ADR + guía en
docs/frontend/api-architecture.md
Referencias
Notas
- Este ADR se creó después de resolver BUG-FRONTEND-001 y BUG-FRONTEND-002
- La estructura actual ya sigue parcialmente este patrón
- Se requiere refactorizar hooks que aún usan hard-coded routes
Versión: 1.0.0 Última actualización: 2025-11-23 Estado: Aceptado Proyecto: GAMILIT - Sistema de Gamificación Educativa