- 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>
15 KiB
15 KiB
ESTÁNDARES DE RUTAS Y CONFIGURACIÓN DE API
Versión: 1.0.0 Fecha: 2025-11-29 Fuente: Consolidado desde orchestration/directivas/ESTANDARES-API-ROUTES.md Audiencia: Backend y Frontend developers
PROBLEMA QUE RESUELVE
Prevenir bugs de rutas duplicadas que generan URLs incorrectas del tipo /api/api/endpoint en lugar de /api/endpoint. Este problema surge por:
- Configuración incorrecta de baseURL en cliente API
- Duplicación de prefijo
/apientre baseURL y endpoints - Falta de estándares claros sobre separación de responsabilidades
- Inconsistencia entre configuración backend y frontend
SEPARACIÓN DE RESPONSABILIDADES
Regla Fundamental
baseURL: Define el protocolo, dominio, puerto y prefijo global (/api)
endpoint: Define solo la ruta específica del recurso (sin prefijos globales)
Responsabilidades Claras
// ✅ CORRECTO - Separación clara
// baseURL contiene:
// - Protocolo (http:// o https://)
// - Dominio/host (localhost, api.gamilit.com)
// - Puerto (si no es default)
// - Prefijo global de API (/api)
const baseURL = 'http://localhost:3000/api';
// endpoint contiene SOLO:
// - Ruta del recurso
// - Sin protocolo, sin dominio, sin puerto
// - Sin prefijo /api
const endpoint = '/health';
const endpoint = '/users';
const endpoint = '/exercises/123';
CONFIGURACIÓN DE API CLIENT
Configuración Correcta con Axios
// ✅ CORRECTO - apps/frontend/web/src/lib/apiClient.ts
import axios from 'axios';
/**
* API Client Configuration
*
* baseURL incluye:
* - Protocolo + dominio + puerto (desde env)
* - Prefijo global /api
*
* Los endpoints NO deben repetir /api
*/
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api`,
// Ejemplo real: 'http://localhost:3000/api'
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Interceptors para logging (opcional)
apiClient.interceptors.request.use(
(config) => {
console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`);
// URL final será: baseURL + url
// Ejemplo: 'http://localhost:3000/api' + '/health' = 'http://localhost:3000/api/health'
return config;
},
(error) => Promise.reject(error)
);
Variables de Entorno
# ✅ CORRECTO - .env (frontend)
# Base URL SIN el prefijo /api
VITE_API_URL=http://localhost:3000
# El prefijo /api se agrega en la configuración del cliente
# NO en la variable de entorno
# ❌ INCORRECTO - NO incluir /api en env
VITE_API_URL=http://localhost:3000/api # ❌ NO HACER ESTO
DEFINICIÓN DE ENDPOINTS
Servicios de API (Frontend)
// ✅ CORRECTO - apps/frontend/web/src/services/exerciseService.ts
import { apiClient } from '@/lib/apiClient';
export const exerciseService = {
/**
* Get all exercises
* GET /api/exercises
*/
async findAll() {
// endpoint sin /api porque baseURL ya lo incluye
const response = await apiClient.get('/exercises');
return response.data;
},
/**
* Get exercise by ID
* GET /api/exercises/:id
*/
async findById(id: string) {
const response = await apiClient.get(`/exercises/${id}`);
return response.data;
},
/**
* Submit exercise answer
* POST /api/exercises/:id/submit
*/
async submitAnswer(id: string, answer: SubmitAnswerDto) {
const response = await apiClient.post(`/exercises/${id}/submit`, answer);
return response.data;
},
};
// ❌ INCORRECTO - NO duplicar /api
export const exerciseService = {
async findAll() {
// ❌ INCORRECTO: genera /api/api/exercises
const response = await apiClient.get('/api/exercises');
return response.data;
},
};
Endpoints con Parámetros
// ✅ CORRECTO - Endpoints con parámetros
export const userService = {
async findById(userId: string) {
// GET /api/users/123
const response = await apiClient.get(`/users/${userId}`);
return response.data;
},
async searchUsers(query: string, page: number = 1) {
// GET /api/users?q=john&page=1
const response = await apiClient.get('/users', {
params: { q: query, page },
});
return response.data;
},
};
Endpoints Anidados
// ✅ CORRECTO - Recursos anidados
export const classroomService = {
// GET /api/classrooms/123/students
async getStudents(classroomId: string) {
const response = await apiClient.get(`/classrooms/${classroomId}/students`);
return response.data;
},
// POST /api/classrooms/123/assignments
async createAssignment(classroomId: string, data: CreateAssignmentDto) {
const response = await apiClient.post(
`/classrooms/${classroomId}/assignments`,
data
);
return response.data;
},
// GET /api/classrooms/123/assignments/456/submissions
async getSubmissions(classroomId: string, assignmentId: string) {
const response = await apiClient.get(
`/classrooms/${classroomId}/assignments/${assignmentId}/submissions`
);
return response.data;
},
};
CONFIGURACIÓN BACKEND (NestJS)
Prefijo Global de API
// ✅ CORRECTO - apps/backend/src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Prefijo global para todas las rutas
app.setGlobalPrefix('api');
await app.listen(3000);
console.log('API running on http://localhost:3000/api');
}
bootstrap();
Controladores
// ✅ CORRECTO - apps/backend/src/modules/exercises/exercises.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
/**
* Exercises Controller
*
* Ruta base: /exercises (sin /api porque se agrega globalmente)
* Rutas finales:
* - GET /api/exercises
* - GET /api/exercises/:id
* - POST /api/exercises/:id/submit
*/
@Controller('exercises') // ✅ Sin prefijo /api
export class ExercisesController {
@Get()
async findAll() {
// Ruta final: GET /api/exercises
return this.exercisesService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
// Ruta final: GET /api/exercises/:id
return this.exercisesService.findById(id);
}
@Post(':id/submit')
async submitAnswer(
@Param('id') id: string,
@Body() dto: SubmitAnswerDto
) {
// Ruta final: POST /api/exercises/:id/submit
return this.exercisesService.submitAnswer(id, dto);
}
}
// ❌ INCORRECTO - NO incluir /api en @Controller
@Controller('api/exercises') // ❌ Genera /api/api/exercises
export class ExercisesController {
// ...
}
Organización de Rutas
// ✅ CORRECTO - Organización de módulos
@Controller('users')
export class UsersController {
@Get() // GET /api/users
@Get(':id') // GET /api/users/:id
@Post() // POST /api/users
@Put(':id') // PUT /api/users/:id
@Delete(':id') // DELETE /api/users/:id
}
@Controller('classrooms')
export class ClassroomsController {
@Get() // GET /api/classrooms
@Get(':id') // GET /api/classrooms/:id
@Get(':id/students') // GET /api/classrooms/:id/students
@Post(':id/assignments') // POST /api/classrooms/:id/assignments
}
@Controller('gamification')
export class GamificationController {
@Get('leaderboard') // GET /api/gamification/leaderboard
@Get('achievements') // GET /api/gamification/achievements
@Post('comodines/use') // POST /api/gamification/comodines/use
}
PATRONES DE URLs
Estructura Estándar
Formato completo de URL:
{protocol}://{domain}:{port}/{globalPrefix}/{controller}/{endpoint}/{params}
Ejemplo:
http://localhost:3000/api/exercises/123/submit
Desglose:
- protocol: http
- domain: localhost
- port: 3000
- globalPrefix: api (configurado en main.ts)
- controller: exercises (definido en @Controller)
- endpoint: 123/submit (definido en @Post)
Ejemplos Correctos
// URLs finales esperadas para GAMILIT:
// Exercises
GET http://localhost:3000/api/exercises
GET http://localhost:3000/api/exercises/123
POST http://localhost:3000/api/exercises/123/submit
// Modules
GET http://localhost:3000/api/modules
GET http://localhost:3000/api/modules/123/exercises
// Gamification
GET http://localhost:3000/api/gamification/user-stats
GET http://localhost:3000/api/gamification/leaderboard
POST http://localhost:3000/api/gamification/comodines/use
GET http://localhost:3000/api/gamification/achievements
// Progress
GET http://localhost:3000/api/progress/modules
GET http://localhost:3000/api/progress/exercises/123
// Auth
POST http://localhost:3000/api/auth/login
POST http://localhost:3000/api/auth/register
POST http://localhost:3000/api/auth/refresh
Ejemplos Incorrectos (Bugs Comunes)
// ❌ INCORRECTO - Duplicación de /api
http://localhost:3000/api/api/exercises // Duplicado
http://localhost:3000/api/api/users // Duplicado
http://localhost:3000/apiapi/modules // Sin separador
// ❌ INCORRECTO - Falta de prefijo
http://localhost:3000/exercises // Falta /api
http://localhost:3000/users // Falta /api
// ❌ INCORRECTO - Prefijo incorrecto
http://localhost:3000/v1/exercises // Prefijo diferente
http://localhost:3000/rest/users // Prefijo diferente
CONFIGURACIÓN POR AMBIENTE
Variables de Entorno por Ambiente
# .env.development (frontend)
VITE_API_URL=http://localhost:3000
# .env.staging (frontend)
VITE_API_URL=https://staging-api.gamilit.com
# .env.production (frontend)
VITE_API_URL=https://api.gamilit.com
Configuración Dinámica
// ✅ CORRECTO - Configuración dinámica por ambiente
// apps/frontend/web/src/config/api.config.ts
export const apiConfig = {
baseURL: `${import.meta.env.VITE_API_URL}/api`,
timeout: 10000,
enableLogging: import.meta.env.DEV,
};
// apps/frontend/web/src/lib/apiClient.ts
import { apiConfig } from '@/config/api.config';
export const apiClient = axios.create(apiConfig);
if (apiConfig.enableLogging) {
apiClient.interceptors.request.use((config) => {
console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`);
return config;
});
}
CORS Y SEGURIDAD
Configuración CORS (Backend)
// ✅ CORRECTO - apps/backend/src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Configurar CORS
app.enableCors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
});
app.setGlobalPrefix('api');
await app.listen(3000);
}
Headers de Seguridad
// ✅ CORRECTO - Configuración de headers
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Redirect to login
window.location.href = '/login';
}
return Promise.reject(error);
}
);
TRAILING SLASHES
Regla de Trailing Slashes
// ✅ CORRECTO - Sin trailing slash al final
const endpoint = '/exercises'; // ✅
const endpoint = '/users/123'; // ✅
const endpoint = '/modules'; // ✅
// ❌ EVITAR - Trailing slash puede causar issues
const endpoint = '/exercises/'; // ❌ Evitar
const endpoint = '/users/123/'; // ❌ Evitar
CHECKLIST DE VALIDACIÓN
Pre-Implementation Checklist
- Verificar que
baseURLincluye protocolo + dominio + puerto +/api - Verificar que
baseURLNO incluye rutas de recursos - Verificar que endpoints NO incluyen prefijo
/api - Verificar que endpoints comienzan con
/ - Verificar que NO hay trailing slashes innecesarios
- Verificar que variables de entorno están configuradas
- Verificar que CORS está configurado correctamente
- Verificar que prefijo global está en
main.tsdel backend
Post-Implementation Checklist
- Probar endpoint en navegador (Network tab)
- Verificar URL final no tiene duplicados (/api/api/)
- Verificar que respuesta es correcta (200 OK)
- Verificar que no hay errores de CORS
- Probar en diferentes ambientes (dev, staging, prod)
- Verificar logs de requests en consola
- Verificar que token de autenticación se envía
- Probar casos de error (404, 500)
Code Review Checklist
- Revisar que NO hay
/apihardcodeado en endpoints - Revisar que
baseURLestá configurado correctamente - Revisar que se usan variables de entorno
- Revisar que NO hay URLs absolutas hardcodeadas
- Revisar consistencia entre backend y frontend
- Revisar que controladores usan rutas relativas
- Revisar que hay logging adecuado
- Revisar que hay manejo de errores
VALIDACIÓN EN RUNTIME
Interceptor para Detectar Duplicaciones
// apps/frontend/web/src/lib/apiClient.ts
apiClient.interceptors.request.use((config) => {
const url = config.url || '';
// Detectar /api/api/
if (url.includes('/api/')) {
console.error(
`[API ERROR] Endpoint contains /api prefix: ${url}\n` +
`This will cause duplicate /api/api/ in final URL.\n` +
`Remove /api from endpoint definition.`
);
if (import.meta.env.DEV) {
throw new Error(`Invalid endpoint: ${url} contains /api prefix`);
}
}
return config;
});
Test Helper
// apps/frontend/web/src/tests/helpers/apiTestHelpers.ts
export function validateEndpoint(endpoint: string) {
if (endpoint.includes('/api/')) {
throw new Error(
`Endpoint "${endpoint}" should not include /api prefix. ` +
`The baseURL already includes /api.`
);
}
if (!endpoint.startsWith('/')) {
throw new Error(
`Endpoint "${endpoint}" should start with /`
);
}
if (endpoint.endsWith('/') && endpoint !== '/') {
console.warn(
`Endpoint "${endpoint}" has trailing slash. Consider removing it.`
);
}
}
// Uso en tests
describe('exerciseService', () => {
it('should use correct endpoint', () => {
const endpoint = '/exercises';
expect(() => validateEndpoint(endpoint)).not.toThrow();
});
it('should reject endpoint with /api prefix', () => {
const endpoint = '/api/exercises';
expect(() => validateEndpoint(endpoint)).toThrow();
});
});
REFERENCIAS
- docs/98-standards/NAMING-CONVENTIONS-COMPLETE.md - Estándares de nomenclatura
- Axios Documentation
- NestJS Controllers
Última actualización: 2025-11-29 Fuente original: orchestration/directivas/ESTANDARES-API-ROUTES.md Mantenido por: Architecture-Analyst