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

9.1 KiB

Convenciones de API REST

Versión: 1.0.0 Última Actualización: 2025-11-28 Aplica a: apps/backend/src/modules/*/controllers/


Resumen

Este documento define las convenciones para diseñar y documentar endpoints REST en GAMILIT. Todos los endpoints siguen estándares RESTful con documentación OpenAPI/Swagger.


Estructura de URLs

Patrón Base

/api/v1/{resource}
/api/v1/{resource}/{id}
/api/v1/{resource}/{id}/{sub-resource}

Ejemplos

Operación Método URL
Listar usuarios GET /api/v1/users
Obtener usuario GET /api/v1/users/:id
Crear usuario POST /api/v1/users
Actualizar usuario PATCH /api/v1/users/:id
Eliminar usuario DELETE /api/v1/users/:id
Logros del usuario GET /api/v1/users/:id/achievements

Códigos de Estado HTTP

Código Significado Cuándo Usar
200 OK GET exitoso, PATCH exitoso
201 Created POST exitoso
204 No Content DELETE exitoso
400 Bad Request Validación fallida
401 Unauthorized No autenticado
403 Forbidden Sin permisos
404 Not Found Recurso no existe
409 Conflict Duplicado o conflicto de estado
422 Unprocessable Entity Lógica de negocio fallida
500 Internal Server Error Error del servidor

Formato de Respuestas

Respuesta Exitosa Simple

{
  "id": "uuid",
  "name": "Ejemplo",
  "createdAt": "2025-11-28T10:00:00Z"
}

Respuesta Exitosa Paginada

{
  "data": [
    { "id": "uuid1", "name": "Item 1" },
    { "id": "uuid2", "name": "Item 2" }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20,
    "totalPages": 5
  }
}

Respuesta de Error

{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    {
      "field": "email",
      "message": "must be a valid email"
    }
  ],
  "timestamp": "2025-11-28T10:00:00Z"
}

Decoradores de Controlador

Controlador Básico

@Controller('api/v1/achievements')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiTags('Achievements')
@ApiBearerAuth()
export class AchievementsController {
  constructor(private readonly achievementsService: AchievementsService) {}
}

Endpoint GET (Listar)

@Get()
@Roles('admin', 'teacher', 'student')
@ApiOperation({ summary: 'List all achievements' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'category', required: false, type: String })
@ApiResponse({ status: 200, description: 'List of achievements', type: [AchievementResponseDto] })
async findAll(
  @Query() query: ListAchievementsDto,
): Promise<PaginatedResponse<AchievementResponseDto>> {
  return this.achievementsService.findAll(query);
}

Endpoint GET (Uno)

@Get(':id')
@Roles('admin', 'teacher', 'student')
@ApiOperation({ summary: 'Get achievement by ID' })
@ApiParam({ name: 'id', type: String, description: 'Achievement UUID' })
@ApiResponse({ status: 200, description: 'Achievement details', type: AchievementResponseDto })
@ApiResponse({ status: 404, description: 'Achievement not found' })
async findOne(
  @Param('id', ParseUUIDPipe) id: string,
): Promise<AchievementResponseDto> {
  return this.achievementsService.findOne(id);
}

Endpoint POST

@Post()
@Roles('admin')
@ApiOperation({ summary: 'Create new achievement' })
@ApiBody({ type: CreateAchievementDto })
@ApiResponse({ status: 201, description: 'Achievement created', type: AchievementResponseDto })
@ApiResponse({ status: 400, description: 'Validation failed' })
async create(
  @Body() createDto: CreateAchievementDto,
  @CurrentUser() user: UserEntity,
): Promise<AchievementResponseDto> {
  return this.achievementsService.create(createDto, user.id);
}

Endpoint PATCH

@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: 'Update achievement' })
@ApiParam({ name: 'id', type: String })
@ApiBody({ type: UpdateAchievementDto })
@ApiResponse({ status: 200, description: 'Achievement updated', type: AchievementResponseDto })
@ApiResponse({ status: 404, description: 'Achievement not found' })
async update(
  @Param('id', ParseUUIDPipe) id: string,
  @Body() updateDto: UpdateAchievementDto,
): Promise<AchievementResponseDto> {
  return this.achievementsService.update(id, updateDto);
}

Endpoint DELETE

@Delete(':id')
@Roles('admin')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete achievement' })
@ApiParam({ name: 'id', type: String })
@ApiResponse({ status: 204, description: 'Achievement deleted' })
@ApiResponse({ status: 404, description: 'Achievement not found' })
async remove(
  @Param('id', ParseUUIDPipe) id: string,
): Promise<void> {
  return this.achievementsService.remove(id);
}

DTOs de Request

CreateDto

export class CreateAchievementDto {
  @ApiProperty({ description: 'Achievement name', example: 'First Steps' })
  @IsString()
  @IsNotEmpty()
  @MaxLength(100)
  name: string;

  @ApiProperty({ description: 'Achievement description' })
  @IsString()
  @IsOptional()
  description?: string;

  @ApiProperty({ description: 'XP reward', example: 50 })
  @IsInt()
  @Min(0)
  @Max(1000)
  xpReward: number;

  @ApiProperty({ description: 'Category ID', format: 'uuid' })
  @IsUUID()
  categoryId: string;
}

UpdateDto

export class UpdateAchievementDto extends PartialType(
  OmitType(CreateAchievementDto, ['categoryId'] as const)
) {}

ListDto (Query Params)

export class ListAchievementsDto extends PaginationDto {
  @ApiPropertyOptional({ description: 'Filter by category' })
  @IsOptional()
  @IsUUID()
  categoryId?: string;

  @ApiPropertyOptional({ description: 'Filter by active status' })
  @IsOptional()
  @Transform(({ value }) => value === 'true')
  @IsBoolean()
  isActive?: boolean;

  @ApiPropertyOptional({ enum: ['name', 'createdAt', 'xpReward'] })
  @IsOptional()
  @IsIn(['name', 'createdAt', 'xpReward'])
  sortBy?: string;
}

DTOs de Response

export class AchievementResponseDto {
  @ApiProperty({ format: 'uuid' })
  id: string;

  @ApiProperty()
  name: string;

  @ApiProperty({ required: false })
  description?: string;

  @ApiProperty()
  xpReward: number;

  @ApiProperty()
  isActive: boolean;

  @ApiProperty({ type: String, format: 'date-time' })
  createdAt: Date;

  @ApiProperty({ type: String, format: 'date-time' })
  updatedAt: Date;
}

Validación

Pipes Globales

// main.ts
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,           // Eliminar propiedades no definidas
    forbidNonWhitelisted: true, // Error si hay propiedades extras
    transform: true,           // Transformar a tipos correctos
    transformOptions: {
      enableImplicitConversion: true,
    },
  }),
);

Pipes Específicos

@Get(':id')
async findOne(
  @Param('id', ParseUUIDPipe) id: string,  // Valida UUID
  @Query('limit', ParseIntPipe) limit: number, // Valida entero
) { }

Documentación Swagger

Acceso

  • URL: http://localhost:3000/api/docs
  • JSON: http://localhost:3000/api/docs-json

Configuración

// main.ts
const config = new DocumentBuilder()
  .setTitle('GAMILIT API')
  .setDescription('API de gamificación educativa')
  .setVersion('2.3.0')
  .addBearerAuth()
  .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);

Endpoints por Módulo

Auth (/api/v1/auth)

  • POST /register - Registrar usuario
  • POST /login - Iniciar sesión
  • POST /logout - Cerrar sesión
  • POST /refresh - Refrescar token
  • POST /forgot-password - Solicitar reset
  • POST /reset-password - Aplicar reset

Gamification (/api/v1/gamification)

  • GET /stats - Estadísticas del usuario
  • GET /achievements - Logros disponibles
  • GET /achievements/user - Logros del usuario
  • GET /leaderboard - Tabla de posiciones
  • GET /ranks - Rangos Maya
  • POST /comodines/purchase - Comprar comodín
  • POST /comodines/use - Usar comodín

Educational (/api/v1/educational)

  • GET /modules - Módulos educativos
  • GET /exercises - Ejercicios
  • POST /exercises/:id/submit - Enviar respuesta

Progress (/api/v1/progress)

  • GET /sessions - Sesiones de aprendizaje
  • GET /submissions - Entregas del usuario
  • GET /module/:id - Progreso por módulo

Buenas Prácticas

  1. Versionado en URL: Siempre /api/v1/
  2. Recursos en plural: /users, no /user
  3. Verbos HTTP correctos: GET lee, POST crea, PATCH actualiza, DELETE elimina
  4. IDs en URL: No en query params para recursos específicos
  5. Filtros en query: ?status=active&page=1
  6. Documentar todo: Cada endpoint con @ApiOperation
  7. Validar entrada: Usar DTOs con class-validator
  8. Respuestas consistentes: Mismo formato siempre

Ver También