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

395 lines
9.1 KiB
Markdown

# 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<PaginatedResponse<AchievementResponseDto>> {
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<AchievementResponseDto> {
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<AchievementResponseDto> {
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<AchievementResponseDto> {
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<void> {
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`