workspace/projects/gamilit/orchestration/directivas/ESTANDARES-API-ROUTES.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

810 lines
19 KiB
Markdown

# ESTÁNDARES DE RUTAS Y CONFIGURACIÓN DE API
**Versión:** 1.0
**Fecha:** 2025-11-23
**Autor:** Architecture-Analyst
**Motivación:** Prevenir bugs de rutas duplicadas y garantizar configuración correcta de endpoints API
---
## PROBLEMA IDENTIFICADO
Se detectó un bug crítico de duplicación de rutas que generaba URLs incorrectas del tipo `/api/api/endpoint` en lugar de `/api/endpoint`. Este problema surgió por:
1. **Configuración incorrecta de baseURL** en cliente API
2. **Duplicación de prefijo `/api`** entre baseURL y definición de endpoints
3. **Falta de estándares claros** sobre separación de responsabilidades
4. **Ausencia de validación** de configuración de rutas
5. **Inconsistencia** entre configuración backend y frontend
---
## ESTÁNDARES Y MEJORES PRÁCTICAS
### 1. SEPARACIÓN DE RESPONSABILIDADES
#### Regla Fundamental
```yaml
baseURL: Define el protocolo, dominio, puerto y prefijo global
endpoint: Define solo la ruta específica del recurso (sin prefijos globales)
```
#### Responsabilidades Claras
```typescript
// ✅ CORRECTO - Separación clara de responsabilidades
// 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 = '/projects/123';
```
---
### 2. CONFIGURACIÓN DE API CLIENT
#### 2.1. Configuración Correcta con Axios
```typescript
// ✅ 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: `${process.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)
);
```
#### 2.2. Configuración con Fetch
```typescript
// ✅ CORRECTO - Si usas fetch nativo
const API_BASE_URL = `${process.env.VITE_API_URL}/api`;
async function apiRequest(endpoint: string, options?: RequestInit) {
// Combina baseURL + endpoint
const url = `${API_BASE_URL}${endpoint}`;
console.log(`[API] ${options?.method || 'GET'} ${url}`);
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
return response.json();
}
```
#### 2.3. Variables de Entorno
```env
# ✅ 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
```
```typescript
// ✅ CORRECTO - Uso en código
const baseURL = `${process.env.VITE_API_URL}/api`;
// Resultado: 'http://localhost:3000/api'
```
```env
# ❌ INCORRECTO - NO incluir /api en env
VITE_API_URL=http://localhost:3000/api # ❌ NO HACER ESTO
```
---
### 3. DEFINICIÓN DE ENDPOINTS
#### 3.1. Servicios de API (Frontend)
```typescript
// ✅ CORRECTO - apps/frontend/web/src/services/healthService.ts
import { apiClient } from '@/lib/apiClient';
export const healthService = {
/**
* Check API health
* GET /api/health
*/
async checkHealth() {
// endpoint sin /api porque baseURL ya lo incluye
const response = await apiClient.get('/health');
return response.data;
},
/**
* Check database health
* GET /api/health/database
*/
async checkDatabase() {
const response = await apiClient.get('/health/database');
return response.data;
},
};
```
```typescript
// ❌ INCORRECTO - NO duplicar /api
export const healthService = {
async checkHealth() {
// ❌ INCORRECTO: genera /api/api/health
const response = await apiClient.get('/api/health');
return response.data;
},
};
```
#### 3.2. Endpoints con Parámetros
```typescript
// ✅ 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 updateUser(userId: string, data: UpdateUserDto) {
// PUT /api/users/123
const response = await apiClient.put(`/users/${userId}`, data);
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;
},
};
```
#### 3.3. Endpoints Anidados
```typescript
// ✅ CORRECTO - Recursos anidados
export const projectService = {
// GET /api/projects/123/tasks
async getProjectTasks(projectId: string) {
const response = await apiClient.get(`/projects/${projectId}/tasks`);
return response.data;
},
// POST /api/projects/123/tasks
async createProjectTask(projectId: string, taskData: CreateTaskDto) {
const response = await apiClient.post(
`/projects/${projectId}/tasks`,
taskData
);
return response.data;
},
// GET /api/projects/123/tasks/456/comments
async getTaskComments(projectId: string, taskId: string) {
const response = await apiClient.get(
`/projects/${projectId}/tasks/${taskId}/comments`
);
return response.data;
},
};
```
---
### 4. CONFIGURACIÓN BACKEND (NestJS)
#### 4.1. Prefijo Global de API
```typescript
// ✅ 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();
```
#### 4.2. Controladores
```typescript
// ✅ CORRECTO - apps/backend/src/modules/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
/**
* Health Controller
*
* Ruta base: /health (sin /api porque se agrega globalmente)
* Rutas finales:
* - GET /api/health
* - GET /api/health/database
*/
@Controller('health') // ✅ Sin prefijo /api
export class HealthController {
@Get()
async checkHealth() {
// Ruta final: GET /api/health
return { status: 'ok', timestamp: new Date() };
}
@Get('database')
async checkDatabase() {
// Ruta final: GET /api/health/database
return { database: 'connected' };
}
}
```
```typescript
// ❌ INCORRECTO - NO incluir /api en @Controller
@Controller('api/health') // ❌ Genera /api/api/health
export class HealthController {
// ...
}
```
#### 4.3. Módulos y Rutas
```typescript
// ✅ 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('projects')
export class ProjectsController {
@Get() // GET /api/projects
@Get(':id') // GET /api/projects/:id
@Get(':id/tasks') // GET /api/projects/:id/tasks
@Post(':id/tasks') // POST /api/projects/:id/tasks
}
```
---
### 5. PATRONES DE URLS
#### 5.1. Estructura Estándar
```yaml
Formato completo de URL:
{protocol}://{domain}:{port}/{globalPrefix}/{controller}/{endpoint}/{params}
Ejemplo:
http://localhost:3000/api/projects/123/tasks
Desglose:
- protocol: http
- domain: localhost
- port: 3000
- globalPrefix: api (configurado en main.ts)
- controller: projects (definido en @Controller)
- endpoint: 123/tasks (definido en @Get)
```
#### 5.2. Ejemplos Correctos
```typescript
// URLs finales esperadas:
GET http://localhost:3000/api/health
GET http://localhost:3000/api/health/database
GET http://localhost:3000/api/users
GET http://localhost:3000/api/users/123
POST http://localhost:3000/api/users
PUT http://localhost:3000/api/users/123
DELETE http://localhost:3000/api/users/123
GET http://localhost:3000/api/projects
GET http://localhost:3000/api/projects/123/tasks
POST http://localhost:3000/api/projects/123/tasks/456/comments
```
#### 5.3. Ejemplos Incorrectos (Bugs Comunes)
```typescript
// ❌ INCORRECTO - Duplicación de /api
http://localhost:3000/api/api/health // Duplicado
http://localhost:3000/api/api/users // Duplicado
http://localhost:3000/apiapi/projects // Sin separador
// ❌ INCORRECTO - Falta de prefijo
http://localhost:3000/health // Falta /api
http://localhost:3000/users // Falta /api
// ❌ INCORRECTO - Prefijo incorrecto
http://localhost:3000/v1/health // Prefijo diferente
http://localhost:3000/rest/users // Prefijo diferente
```
---
### 6. CONFIGURACIÓN POR AMBIENTE
#### 6.1. Variables de Entorno por Ambiente
```env
# .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
```
```env
# .env.development (backend)
PORT=3000
NODE_ENV=development
# .env.staging (backend)
PORT=3000
NODE_ENV=staging
# .env.production (backend)
PORT=3000
NODE_ENV=production
```
#### 6.2. Configuración Dinámica
```typescript
// ✅ 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,
// Logging solo en desarrollo
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;
});
}
```
---
### 7. CORS Y SEGURIDAD
#### 7.1. Configuración CORS (Backend)
```typescript
// ✅ 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);
}
```
#### 7.2. Headers de Seguridad
```typescript
// ✅ 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);
}
);
```
---
### 8. TRAILING SLASHES
#### 8.1. Regla de Trailing Slashes
```typescript
// ✅ CORRECTO - Sin trailing slash al final
const endpoint = '/health'; // ✅
const endpoint = '/users/123'; // ✅
const endpoint = '/projects'; // ✅
// ❌ EVITAR - Trailing slash puede causar issues
const endpoint = '/health/'; // ❌ Evitar
const endpoint = '/users/123/'; // ❌ Evitar
```
#### 8.2. Configuración de Trailing Slashes
```typescript
// ✅ CORRECTO - Normalizar trailing slashes
function normalizeEndpoint(endpoint: string): string {
// Eliminar trailing slash
return endpoint.replace(/\/$/, '');
}
export const apiClient = axios.create({
baseURL: normalizeEndpoint(`${process.env.VITE_API_URL}/api`),
});
```
---
## EJEMPLOS
### Ejemplo Completo: Módulo de Health Check
#### Backend
```typescript
// apps/backend/src/modules/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthService } from './health.service';
@Controller('health') // Ruta base: /health
export class HealthController {
constructor(private readonly healthService: HealthService) {}
@Get() // GET /api/health
async checkHealth() {
return this.healthService.checkHealth();
}
@Get('database') // GET /api/health/database
async checkDatabase() {
return this.healthService.checkDatabase();
}
}
```
#### Frontend - Configuración
```typescript
// apps/frontend/web/src/lib/apiClient.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api`,
timeout: 10000,
});
```
#### Frontend - Service
```typescript
// apps/frontend/web/src/services/healthService.ts
import { apiClient } from '@/lib/apiClient';
export const healthService = {
async checkHealth() {
const response = await apiClient.get('/health');
return response.data;
},
async checkDatabase() {
const response = await apiClient.get('/health/database');
return response.data;
},
};
```
#### Frontend - Uso en Componente
```typescript
// apps/frontend/web/src/components/HealthCheck.tsx
import { useEffect, useState } from 'react';
import { healthService } from '@/services/healthService';
export const HealthCheck = () => {
const [health, setHealth] = useState(null);
useEffect(() => {
const checkHealth = async () => {
const data = await healthService.checkHealth();
setHealth(data);
};
checkHealth();
}, []);
return <div>Health: {health?.status}</div>;
};
```
---
## CHECKLIST DE VALIDACIÓN
### Pre-Implementation Checklist
- [ ] Verificar que `baseURL` incluye protocolo + dominio + puerto + `/api`
- [ ] Verificar que `baseURL` NO 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.ts` del 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 `/api` hardcodeado en endpoints
- [ ] Revisar que `baseURL` está 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
---
## HERRAMIENTAS DE AUTOMATIZACIÓN
### 1. ESLint Rule - No API Prefix in Endpoints
```javascript
// .eslintrc.js - Custom rule
module.exports = {
rules: {
'no-api-prefix-in-endpoints': {
create(context) {
return {
CallExpression(node) {
// Detectar apiClient.get('/api/...')
if (
node.callee?.object?.name === 'apiClient' &&
node.arguments?.[0]?.type === 'Literal' &&
typeof node.arguments[0].value === 'string' &&
node.arguments[0].value.startsWith('/api/')
) {
context.report({
node,
message: 'Endpoint should not include /api prefix. Remove /api from endpoint path.',
});
}
},
};
},
},
},
};
```
### 2. Test Helpers
```typescript
// 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('healthService', () => {
it('should use correct endpoint', () => {
const endpoint = '/health';
expect(() => validateEndpoint(endpoint)).not.toThrow();
});
it('should reject endpoint with /api prefix', () => {
const endpoint = '/api/health';
expect(() => validateEndpoint(endpoint)).toThrow();
});
});
```
### 3. Runtime Validation
```typescript
// apps/frontend/web/src/lib/apiClient.ts
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api`,
});
// Interceptor para detectar duplicaciones
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;
});
```
### 4. Pre-commit Hook
```bash
#!/bin/bash
# .git/hooks/pre-commit
echo "Validating API endpoints..."
# Buscar /api/ en endpoints de servicios
if git diff --cached --name-only | grep -E 'services/.*Service\.ts$'; then
if git diff --cached | grep -E "apiClient\.(get|post|put|delete|patch)\(['\"]\/api\/"; then
echo "ERROR: Found /api prefix in endpoint definition"
echo "Endpoints should not include /api prefix"
echo "The baseURL already includes /api"
exit 1
fi
fi
echo "API endpoint validation passed"
```
---
## REFERENCIAS
- [ESTANDARES-NOMENCLATURA.md](./ESTANDARES-NOMENCLATURA.md) - Estándares de nomenclatura
- [CHECKLIST-CODE-REVIEW-API.md](./CHECKLIST-CODE-REVIEW-API.md) - Checklist de code review
- [ESTANDARES-TESTING-API.md](./ESTANDARES-TESTING-API.md) - Estándares de testing
- [PITFALLS-API-ROUTES.md](./PITFALLS-API-ROUTES.md) - Errores comunes
- [Axios Documentation](https://axios-http.com/docs/intro)
- [NestJS Controllers](https://docs.nestjs.com/controllers)
---
**Uso:** Referencia obligatoria para configuración de rutas API
**Validación:** Obligatoria en code reviews y antes de merge
**Actualización:** Mantener sincronizado con cambios en arquitectura