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

19 KiB

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

baseURL: Define el protocolo, dominio, puerto y prefijo global
endpoint: Define solo la ruta específica del recurso (sin prefijos globales)

Responsabilidades Claras

// ✅ 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

// ✅ 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

// ✅ 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

# ✅ 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
// ✅ CORRECTO - Uso en código

const baseURL = `${process.env.VITE_API_URL}/api`;
// Resultado: 'http://localhost:3000/api'
# ❌ 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)

// ✅ 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;
  },
};
// ❌ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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' };
  }
}
// ❌ INCORRECTO - NO incluir /api en @Controller

@Controller('api/health')  // ❌ Genera /api/api/health
export class HealthController {
  // ...
}

4.3. Módulos y 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('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

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

// 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)

// ❌ 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.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.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

// ✅ 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)

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// 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

// 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

// 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

// 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

// .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

// 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

// 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

#!/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


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