workspace/projects/gamilit/orchestration/directivas/PITFALLS-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

867 lines
18 KiB
Markdown

# ERRORES COMUNES EN RUTAS API
**Versión:** 1.0
**Fecha:** 2025-11-23
**Autor:** Architecture-Analyst
**Motivación:** Documentar errores comunes y sus soluciones para prevenir bugs recurrentes
---
## PROBLEMA IDENTIFICADO
Se han identificado patrones recurrentes de errores en configuración de rutas API que causan bugs en producción. Este documento cataloga los **errores más comunes**, sus **síntomas**, **causas raíz** y **soluciones**.
---
## CATEGORÍAS DE ERRORES
### 1. DUPLICACIÓN DE PREFIJOS `/api`
#### 1.1. Duplicación en Frontend
**Síntoma:**
```
Request URL: http://localhost:3000/api/api/health
Status: 404 Not Found
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - apps/frontend/web/src/lib/apiClient.ts
export const apiClient = axios.create({
baseURL: `${process.env.VITE_API_URL}/api`, // Ya incluye /api
});
// ❌ INCORRECTO - apps/frontend/web/src/services/healthService.ts
export const healthService = {
async checkHealth() {
// Duplica /api porque apiClient.baseURL ya lo incluye
const response = await apiClient.get('/api/health'); // ❌
return response.data;
},
};
```
**Resultado:**
```
Final URL: http://localhost:3000/api + /api/health = /api/api/health
```
**Solución:**
```typescript
// ✅ CORRECTO - apps/frontend/web/src/services/healthService.ts
export const healthService = {
async checkHealth() {
// Sin /api porque baseURL ya lo incluye
const response = await apiClient.get('/health'); // ✅
return response.data;
},
};
```
**Resultado:**
```
Final URL: http://localhost:3000/api + /health = /api/health ✅
```
#### 1.2. Duplicación en Backend
**Síntoma:**
```
Endpoint esperado: GET /api/health
Endpoint real: GET /api/api/health
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - apps/backend/src/modules/health/health.controller.ts
@Controller('api/health') // ❌ Ya incluye /api
export class HealthController {
@Get()
async checkHealth() {
// Genera: GET /api/api/health
return { status: 'ok' };
}
}
```
**Solución:**
```typescript
// ✅ CORRECTO
@Controller('health') // ✅ Sin /api porque se agrega globalmente
export class HealthController {
@Get()
async checkHealth() {
// Genera: GET /api/health
return { status: 'ok' };
}
}
// En main.ts debe estar configurado:
app.setGlobalPrefix('api');
```
---
### 2. MIXING RELATIVE AND ABSOLUTE PATHS
#### 2.1. Paths Absolutos en Endpoints
**Síntoma:**
```
TypeError: Cannot read property 'baseURL' of undefined
Error: Network request failed
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - Usar URL absoluta en vez de path relativo
export const healthService = {
async checkHealth() {
const response = await apiClient.get(
'http://localhost:3000/api/health' // ❌ URL absoluta
);
return response.data;
},
};
```
**Problemas:**
1. Ignora configuración de `baseURL`
2. Hardcodea URL (no funciona en diferentes ambientes)
3. No usa interceptors configurados
4. Dificulta testing y mocking
**Solución:**
```typescript
// ✅ CORRECTO - Usar path relativo
export const healthService = {
async checkHealth() {
const response = await apiClient.get('/health'); // ✅ Path relativo
return response.data;
},
};
```
#### 2.2. Falta de Slash Inicial
**Síntoma:**
```
Request URL: http://localhost:3000/apihealth (sin separación)
Status: 404 Not Found
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - Sin slash inicial
export const healthService = {
async checkHealth() {
const response = await apiClient.get('health'); // ❌ Sin /
return response.data;
},
};
```
**Resultado:**
```
baseURL: http://localhost:3000/api
endpoint: health
Final: http://localhost:3000/apihealth ❌
```
**Solución:**
```typescript
// ✅ CORRECTO - Con slash inicial
export const healthService = {
async checkHealth() {
const response = await apiClient.get('/health'); // ✅ Con /
return response.data;
},
};
```
**Resultado:**
```
baseURL: http://localhost:3000/api
endpoint: /health
Final: http://localhost:3000/api/health ✅
```
---
### 3. HARDCODED URLs
#### 3.1. URLs Hardcodeadas en Código
**Síntoma:**
```
Error: Network request failed (en staging/prod)
CORS error (dominio incorrecto)
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - URL hardcodeada
export const apiClient = axios.create({
baseURL: 'http://localhost:3000/api', // ❌ Hardcoded
});
```
**Problemas:**
1. No funciona en staging/producción
2. Dificulta testing
3. Requiere cambios de código para cada ambiente
4. Propenso a errores
**Solución:**
```typescript
// ✅ CORRECTO - Usar variables de entorno
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api`, // ✅ Desde env
});
```
```env
# .env.development
VITE_API_URL=http://localhost:3000
# .env.staging
VITE_API_URL=https://staging-api.gamilit.com
# .env.production
VITE_API_URL=https://api.gamilit.com
```
#### 3.2. URLs en Variables de Entorno Incorrectas
**Síntoma:**
```
Request URL: http://localhost:3000/api/api/health
```
**Causa Raíz:**
```env
# ❌ INCORRECTO - Ya incluye /api en variable
VITE_API_URL=http://localhost:3000/api
```
```typescript
// Configuración
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api`,
// Resultado: http://localhost:3000/api/api
});
```
**Solución:**
```env
# ✅ CORRECTO - Sin /api en variable
VITE_API_URL=http://localhost:3000
```
```typescript
// Configuración
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api`,
// Resultado: http://localhost:3000/api ✅
});
```
---
### 4. TRAILING SLASHES
#### 4.1. Trailing Slash en baseURL
**Síntoma:**
```
Request URL: http://localhost:3000/api//health (doble slash)
```
**Causa Raíz:**
```typescript
// ❌ PROBLEMA - Trailing slash en baseURL
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api/`, // ❌ / al final
});
// Endpoint
apiClient.get('/health');
// Resultado: http://localhost:3000/api//health ❌
```
**Solución:**
```typescript
// ✅ CORRECTO - Sin trailing slash
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api`, // ✅ Sin / al final
});
// Endpoint
apiClient.get('/health');
// Resultado: http://localhost:3000/api/health ✅
```
#### 4.2. Trailing Slash en Endpoints
**Síntoma:**
```
Algunas veces funciona, otras veces 404
Inconsistencia entre ambientes
```
**Causa Raíz:**
```typescript
// ⚠️ INCONSISTENTE - Algunos con /, otros sin
export const userService = {
async findAll() {
return await apiClient.get('/users/'); // Con /
},
async findById(id: string) {
return await apiClient.get(`/users/${id}`); // Sin /
},
};
```
**Solución:**
```typescript
// ✅ CORRECTO - Consistente, sin trailing slash
export const userService = {
async findAll() {
return await apiClient.get('/users'); // ✅ Sin /
},
async findById(id: string) {
return await apiClient.get(`/users/${id}`); // ✅ Sin /
},
};
```
---
### 5. CORS CONFIGURATION MISMATCHES
#### 5.1. Origin Mismatch
**Síntoma:**
```
Access to fetch at 'http://localhost:3000/api/health' from origin
'http://localhost:5173' has been blocked by CORS policy
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - CORS no permite origin del frontend
app.enableCors({
origin: 'http://localhost:3001', // ❌ Puerto incorrecto
});
// Frontend corre en:
// http://localhost:5173 ❌ No está permitido
```
**Solución:**
```typescript
// ✅ CORRECTO - Permitir origin correcto
app.enableCors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173', // ✅
credentials: true,
});
```
#### 5.2. Missing CORS Headers
**Síntoma:**
```
Request header field authorization is not allowed by
Access-Control-Allow-Headers
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - No permite header Authorization
app.enableCors({
origin: 'http://localhost:5173',
allowedHeaders: ['Content-Type'], // ❌ Falta Authorization
});
```
**Solución:**
```typescript
// ✅ CORRECTO - Incluir todos los headers necesarios
app.enableCors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization'], // ✅
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
});
```
---
### 6. BASE URL PORT MISMATCHES
#### 6.1. Puerto Incorrecto
**Síntoma:**
```
Error: connect ECONNREFUSED 127.0.0.1:3001
Network request failed
```
**Causa Raíz:**
```env
# ❌ INCORRECTO - Puerto no coincide con backend
VITE_API_URL=http://localhost:3001
```
```typescript
// Backend corre en puerto 3000
await app.listen(3000);
```
**Solución:**
```env
# ✅ CORRECTO - Puerto coincide
VITE_API_URL=http://localhost:3000
```
#### 6.2. Puerto Default vs Explícito
**Síntoma:**
```
Request works in dev but fails in prod
CORS errors in production
```
**Causa Raíz:**
```env
# ❌ PROBLEMA - Asume puerto default
VITE_API_URL=https://api.gamilit.com # ¿Puerto 443 (default HTTPS)?
```
```typescript
// Pero backend corre en puerto custom
await app.listen(8080); // Puerto custom
```
**Solución:**
```env
# ✅ CORRECTO - Especificar puerto si no es default
VITE_API_URL=https://api.gamilit.com:8080
# O usar puerto default (443 para HTTPS)
VITE_API_URL=https://api.gamilit.com
```
---
### 7. INTERCEPTOR ISSUES
#### 7.1. Modificación Incorrecta de URL
**Síntoma:**
```
Request URL cambió inesperadamente
Headers o params perdidos
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - Modifica URL sin retornar config
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
config.headers.Authorization = `Bearer ${token}`;
// ❌ No retorna config
});
```
**Solución:**
```typescript
// ✅ CORRECTO - Retorna config
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config; // ✅ Retornar config
});
```
#### 7.2. Interceptor que Duplica Prefijos
**Síntoma:**
```
Request URL: http://localhost:3000/api/api/health
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - Agrega /api en interceptor
apiClient.interceptors.request.use((config) => {
config.url = `/api${config.url}`; // ❌ Duplica /api
return config;
});
```
**Solución:**
```typescript
// ✅ CORRECTO - No modificar URL en interceptor
apiClient.interceptors.request.use((config) => {
// Solo agregar headers, token, etc.
// NO modificar URL
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
```
---
### 8. TEMPLATE LITERAL ERRORS
#### 8.1. Concatenación en vez de Template Literals
**Síntoma:**
```
Request URL: http://localhost:3000/api/users/[object Object]
TypeError: Cannot convert object to primitive value
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - Concatenación puede causar issues
export const userService = {
async findById(id: string) {
const response = await apiClient.get('/users/' + id); // ❌
return response.data;
},
};
// Peor aún - pasar objeto
await userService.findById({ id: '123' }); // ❌
// Resultado: /users/[object Object]
```
**Solución:**
```typescript
// ✅ CORRECTO - Template literals con validación
export const userService = {
async findById(id: string) {
if (!id || typeof id !== 'string') {
throw new Error('Invalid user ID');
}
const response = await apiClient.get(`/users/${id}`); // ✅
return response.data;
},
};
// Uso correcto
await userService.findById('123'); // ✅
```
#### 8.2. IDs no Sanitizados
**Síntoma:**
```
Request URL: http://localhost:3000/api/users/123%20OR%201=1
SQL Injection attempt
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - No sanitiza input
export const userService = {
async findById(id: string) {
// id podría ser: "123 OR 1=1"
const response = await apiClient.get(`/users/${id}`); // ❌
return response.data;
},
};
```
**Solución:**
```typescript
// ✅ CORRECTO - Validar y sanitizar
export const userService = {
async findById(id: string) {
// Validar formato UUID
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!UUID_REGEX.test(id)) {
throw new Error('Invalid user ID format');
}
const response = await apiClient.get(`/users/${id}`); // ✅
return response.data;
},
};
```
---
### 9. QUERY PARAMETERS ISSUES
#### 9.1. Concatenación Manual de Query Params
**Síntoma:**
```
Request URL: http://localhost:3000/api/users?q=john doe&page=1 (sin encoding)
Search fails for multi-word queries
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - Concatenación manual sin encoding
export const userService = {
async searchUsers(query: string, page: number) {
const response = await apiClient.get(
`/users?q=${query}&page=${page}` // ❌ No encodes query
);
return response.data;
},
};
// query = "john doe"
// Resultado: /users?q=john doe&page=1 ❌ (espacio sin encodear)
```
**Solución:**
```typescript
// ✅ CORRECTO - Usar objeto params (auto-encode)
export const userService = {
async searchUsers(query: string, page: number = 1) {
const response = await apiClient.get('/users', {
params: { q: query, page }, // ✅ Auto-encoding
});
return response.data;
},
};
// query = "john doe"
// Resultado: /users?q=john%20doe&page=1 ✅
```
#### 9.2. Params Undefined
**Síntoma:**
```
Request URL: http://localhost:3000/api/users?page=undefined
Backend error: Invalid page number
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - No valida params undefined
export const userService = {
async findAll(page?: number) {
const response = await apiClient.get('/users', {
params: { page }, // ❌ page puede ser undefined
});
return response.data;
},
};
```
**Solución:**
```typescript
// ✅ CORRECTO - Filtrar params undefined
export const userService = {
async findAll(page?: number) {
const params: Record<string, any> = {};
if (page !== undefined) {
params.page = page;
}
const response = await apiClient.get('/users', { params });
return response.data;
},
};
// O usar default value
export const userService = {
async findAll(page: number = 1) { // ✅ Default value
const response = await apiClient.get('/users', {
params: { page },
});
return response.data;
},
};
```
---
### 10. ERROR HANDLING
#### 10.1. No Error Handling
**Síntoma:**
```
Unhandled Promise Rejection
Application crashes
User sees technical error
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - No maneja errores
export const userService = {
async findById(id: string) {
const response = await apiClient.get(`/users/${id}`); // ❌ Sin try/catch
return response.data;
},
};
```
**Solución:**
```typescript
// ✅ CORRECTO - Manejo de errores apropiado
export const userService = {
async findById(id: string): Promise<User> {
try {
const response = await apiClient.get<User>(`/users/${id}`);
return response.data;
} catch (error) {
console.error('[UserService] Error fetching user:', error);
// Re-throw con mensaje user-friendly
throw new Error('Failed to fetch user. Please try again.');
}
},
};
```
#### 10.2. Exponer Errores Internos
**Síntoma:**
```
User sees: "TypeError: Cannot read property 'data' of undefined"
Security issue: exposes internal structure
```
**Causa Raíz:**
```typescript
// ❌ INCORRECTO - Expone error técnico
export const userService = {
async findById(id: string) {
try {
const response = await apiClient.get(`/users/${id}`);
return response.data;
} catch (error) {
throw error; // ❌ Expone error técnico
}
},
};
```
**Solución:**
```typescript
// ✅ CORRECTO - Error user-friendly
export const userService = {
async findById(id: string): Promise<User> {
try {
const response = await apiClient.get<User>(`/users/${id}`);
return response.data;
} catch (error) {
// Log técnico (solo dev)
if (import.meta.env.DEV) {
console.error('[UserService] Error:', error);
}
// Error user-friendly
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
throw new Error('User not found');
}
if (error.response?.status === 500) {
throw new Error('Server error. Please try again later.');
}
}
throw new Error('Failed to fetch user');
}
},
};
```
---
## DETECCIÓN TEMPRANA
### Checklist de Auto-Validación
Antes de commit, verificar:
- [ ] NO hay `/api` en endpoints de servicios
- [ ] Todos los endpoints empiezan con `/`
- [ ] NO hay URLs absolutas hardcodeadas
- [ ] Se usan variables de entorno
- [ ] NO hay trailing slashes inconsistentes
- [ ] Hay manejo de errores
- [ ] Query params usan objeto `params`
- [ ] IDs se validan antes de usar en URLs
### Herramientas de Detección
```typescript
// Setup en apiClient para detectar errores comunes
export const apiClient = axios.create({
baseURL: `${import.meta.env.VITE_API_URL}/api`,
});
// Interceptor para detectar problemas
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` +
`Remove /api from endpoint definition.`
);
if (import.meta.env.DEV) {
throw new Error(`Invalid endpoint: ${url}`);
}
}
// Detectar URL absoluta
if (url.startsWith('http://') || url.startsWith('https://')) {
console.warn(
`[API WARNING] Using absolute URL: ${url}\n` +
`Consider using relative path instead.`
);
}
// Detectar falta de slash inicial
if (url && !url.startsWith('/')) {
console.warn(
`[API WARNING] Endpoint missing leading slash: ${url}\n` +
`This may cause incorrect URL concatenation.`
);
}
return config;
});
```
---
## REFERENCIAS
- [ESTANDARES-API-ROUTES.md](./ESTANDARES-API-ROUTES.md) - Estándares de rutas API
- [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
- [AUTOMATIZACION-VALIDACION-RUTAS.md](./AUTOMATIZACION-VALIDACION-RUTAS.md) - Automatización
---
**Uso:** Consultar al encontrar errores de API
**Actualización:** Agregar nuevos pitfalls conforme se descubran
**Mantenimiento:** Revisar y actualizar cada trimestre