workspace/projects/gamilit/docs/95-guias-desarrollo/backend/API-STANDARDS.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

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:

  1. Configuración incorrecta de baseURL en cliente API
  2. Duplicación de prefijo /api entre baseURL y endpoints
  3. Falta de estándares claros sobre separación de responsabilidades
  4. 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 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

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


Última actualización: 2025-11-29 Fuente original: orchestration/directivas/ESTANDARES-API-ROUTES.md Mantenido por: Architecture-Analyst