# 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 ```json { "id": "uuid", "name": "Ejemplo", "createdAt": "2025-11-28T10:00:00Z" } ``` ### Respuesta Exitosa Paginada ```json { "data": [ { "id": "uuid1", "name": "Item 1" }, { "id": "uuid2", "name": "Item 2" } ], "meta": { "total": 100, "page": 1, "limit": 20, "totalPages": 5 } } ``` ### Respuesta de Error ```json { "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 ```typescript @Controller('api/v1/achievements') @UseGuards(JwtAuthGuard, RolesGuard) @ApiTags('Achievements') @ApiBearerAuth() export class AchievementsController { constructor(private readonly achievementsService: AchievementsService) {} } ``` ### Endpoint GET (Listar) ```typescript @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> { return this.achievementsService.findAll(query); } ``` ### Endpoint GET (Uno) ```typescript @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 { return this.achievementsService.findOne(id); } ``` ### Endpoint POST ```typescript @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 { return this.achievementsService.create(createDto, user.id); } ``` ### Endpoint PATCH ```typescript @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 { return this.achievementsService.update(id, updateDto); } ``` ### Endpoint DELETE ```typescript @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 { return this.achievementsService.remove(id); } ``` --- ## DTOs de Request ### CreateDto ```typescript 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 ```typescript export class UpdateAchievementDto extends PartialType( OmitType(CreateAchievementDto, ['categoryId'] as const) ) {} ``` ### ListDto (Query Params) ```typescript 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 ```typescript 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 ```typescript // 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 ```typescript @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 ```typescript // 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 - [ERROR-HANDLING.md](./ERROR-HANDLING.md) - Manejo de errores - [ESTRUCTURA-MODULOS.md](./ESTRUCTURA-MODULOS.md) - Estructura de módulos - Swagger UI: `http://localhost:3000/api/docs`