workspace/projects/gamilit/docs/01-fase-alcance-inicial/EAI-001-fundamentos/historias-usuario/US-FUND-006-api-restful-basica.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

11 KiB

US-FUND-006: API RESTful básica

Épica: EAI-001 - Fundamentos Sprint: Mes 1, Semana 1-2 Story Points: 10 SP Presupuesto: $3,600 MXN Prioridad: Alta (Alcance Inicial) Estado: Completada (Mes 1)


Descripción

Como desarrollador, necesito una API RESTful bien estructurada y consistente para facilitar el desarrollo del frontend y garantizar mantenibilidad del código.

Contexto del Alcance Inicial: Esta historia establece los estándares y patrones de la API REST que se usarán en todo el proyecto: estructura de endpoints, middleware de autenticación, validación de datos, manejo de errores, y documentación con Swagger. Es fundamental para mantener consistencia en el desarrollo.


Criterios de Aceptación

Estructura de API

  • CA-01: Todos los endpoints siguen convenciones RESTful (GET, POST, PATCH, DELETE)
  • CA-02: URLs consistentes: /api/v1/resource o /api/v1/resource/:id
  • CA-03: Respuestas con formato JSON consistente
  • CA-04: Códigos de estado HTTP apropiados (200, 201, 400, 401, 404, 500)

Validación

  • CA-05: Todos los DTOs usan class-validator para validación
  • CA-06: Validación automática en todos los endpoints
  • CA-07: Mensajes de error descriptivos en validaciones

Autenticación

  • CA-08: Middleware de autenticación JWT funcional
  • CA-09: Decorador @Public() para endpoints sin autenticación
  • CA-10: Decorador @Roles() para control de acceso por rol

Manejo de Errores

  • CA-11: Global exception filter configurado
  • CA-12: Errores formateados consistentemente
  • CA-13: Logging de errores con contexto

Documentación

  • CA-14: Swagger UI accesible en /api/docs
  • CA-15: Todos los endpoints documentados con decoradores
  • CA-16: Ejemplos de request/response en documentación

Especificaciones Técnicas

Estructura de Respuestas

Success Response:

// GET /api/users/123
{
  "data": {
    "id": "123",
    "email": "user@example.com",
    "firstName": "John",
    "lastName": "Doe"
  }
}

// GET /api/modules (lista)
{
  "data": [
    { "id": "1", "title": "Módulo 1" },
    { "id": "2", "title": "Módulo 2" }
  ],
  "meta": {
    "total": 2,
    "page": 1,
    "limit": 10
  }
}

Error Response:

// 400 Bad Request
{
  "statusCode": 400,
  "message": ["email must be an email", "password is too short"],
  "error": "Bad Request",
  "timestamp": "2025-11-02T12:00:00.000Z",
  "path": "/api/auth/register"
}

// 401 Unauthorized
{
  "statusCode": 401,
  "message": "Invalid credentials",
  "error": "Unauthorized"
}

// 404 Not Found
{
  "statusCode": 404,
  "message": "User not found",
  "error": "Not Found"
}

DTOs y Validación

Ejemplo de DTO:

// dtos/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsEnum } from 'class-validator'
import { ApiProperty } from '@nestjs/swagger'

export class CreateUserDto {
  @ApiProperty({ example: 'user@example.com' })
  @IsEmail()
  email: string

  @ApiProperty({ example: 'SecurePassword123', minLength: 8 })
  @IsString()
  @MinLength(8)
  password: string

  @ApiProperty({ example: 'John' })
  @IsString()
  firstName: string

  @ApiProperty({ example: 'Doe' })
  @IsString()
  lastName: string

  @ApiProperty({ enum: ['student', 'teacher'], example: 'student' })
  @IsEnum(['student', 'teacher'])
  role: 'student' | 'teacher'
}

// dtos/update-user.dto.ts
import { PartialType } from '@nestjs/swagger'

export class UpdateUserDto extends PartialType(CreateUserDto) {
  // Todos los campos son opcionales
}

Guards y Decoradores

JWT Auth Guard:

// guards/jwt-auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { AuthGuard } from '@nestjs/passport'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super()
  }

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler())
    if (isPublic) return true

    return super.canActivate(context)
  }
}

Roles Guard:

// guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler())
    if (!requiredRoles) return true

    const request = context.switchToHttp().getRequest()
    const user = request.user

    return requiredRoles.includes(user.role)
  }
}

Decoradores Personalizados:

// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common'

export const Public = () => SetMetadata('isPublic', true)

// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common'

export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest()
    return request.user
  }
)

Uso en Controllers:

@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
  @Get('profile')
  getProfile(@CurrentUser() user: User) {
    return { data: user }
  }

  @Patch('profile')
  updateProfile(
    @CurrentUser() user: User,
    @Body() updateDto: UpdateUserDto
  ) {
    return this.usersService.update(user.id, updateDto)
  }

  @Get()
  @Roles('teacher', 'admin')
  getAllUsers() {
    return this.usersService.findAll()
  }
}

@Controller('auth')
export class AuthController {
  @Post('login')
  @Public()
  login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto)
  }
}

Global Exception Filter

// filters/http-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger
} from '@nestjs/common'

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name)

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse()
    const request = ctx.getRequest()

    let status = HttpStatus.INTERNAL_SERVER_ERROR
    let message: string | string[] = 'Internal server error'

    if (exception instanceof HttpException) {
      status = exception.getStatus()
      const exceptionResponse = exception.getResponse()

      message = typeof exceptionResponse === 'string'
        ? exceptionResponse
        : (exceptionResponse as any).message || exception.message
    }

    const errorResponse = {
      statusCode: status,
      message,
      error: HttpStatus[status],
      timestamp: new Date().toISOString(),
      path: request.url
    }

    // Log de error
    this.logger.error(
      `${request.method} ${request.url}`,
      JSON.stringify(errorResponse),
      exception instanceof Error ? exception.stack : ''
    )

    response.status(status).json(errorResponse)
  }
}

Swagger Configuration

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  // Global prefix
  app.setGlobalPrefix('api/v1')

  // Validation pipe
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true
    })
  )

  // Exception filter
  app.useGlobalFilters(new HttpExceptionFilter())

  // Global guards
  const reflector = app.get(Reflector)
  app.useGlobalGuards(new JwtAuthGuard(reflector))

  // Swagger
  const config = new DocumentBuilder()
    .setTitle('GAMILIT API')
    .setDescription('API para plataforma educativa gamificada GAMILIT')
    .setVersion('1.0')
    .addBearerAuth()
    .addTag('auth', 'Endpoints de autenticación')
    .addTag('users', 'Gestión de usuarios')
    .addTag('modules', 'Módulos educativos')
    .addTag('activities', 'Actividades')
    .addTag('gamification', 'Sistema de gamificación')
    .build()

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

  await app.listen(4000)
}

Dependencias

Antes:

  • US-FUND-004 (Infraestructura)

Después:

  • Base para todos los módulos de la aplicación

Definición de Hecho (DoD)

  • Global validation pipe configurado
  • Exception filter implementado
  • Guards (JWT, Roles) funcionando
  • Decoradores personalizados creados
  • Swagger configurado y accesible
  • Formato de respuestas estandarizado
  • DTOs con validaciones en todos los módulos
  • Tests unitarios para guards y filters
  • Documentación de estándares API

Notas del Alcance Inicial

  • API REST estándar (no GraphQL)
  • Versionado simple (v1 en prefix)
  • Sin rate limiting avanzado
  • Sin API key authentication
  • Sin caching layer (Redis)
  • Sin compresión de respuestas (gzip)
  • ⚠️ Extensión futura: EXT-012-API (rate limiting, caching, compresión)
  • ⚠️ Extensión futura: EXT-013-GraphQL (API GraphQL alternativa)

Testing

Tests Unitarios

describe('HttpExceptionFilter', () => {
  it('should format HttpException correctly')
  it('should handle unknown errors')
  it('should log errors with context')
})

describe('JwtAuthGuard', () => {
  it('should allow public routes')
  it('should reject unauthenticated requests')
  it('should allow authenticated requests')
})

describe('RolesGuard', () => {
  it('should allow if no roles required')
  it('should allow if user has required role')
  it('should reject if user lacks required role')
})

Tests E2E

describe('API Standards', () => {
  it('GET /api/v1/users - returns formatted response')
  it('POST /api/v1/users - validates DTO')
  it('POST /api/v1/users - returns validation errors')
  it('GET /api/v1/protected - returns 401 without token')
  it('GET /api/v1/admin - returns 403 for non-admin')
})

Estimación

Desglose de Esfuerzo (10 SP = ~3.5 días):

  • Guards y decoradores: 1 día
  • Exception filter: 0.5 días
  • DTOs y validaciones: 1 día
  • Swagger configuration: 0.5 días
  • Estandarización de respuestas: 0.5 días
  • Testing: 0.75 días
  • Documentación: 0.25 días

Riesgos:

  • Cambios en estándares pueden requerir refactoring
  • Swagger puede ser complejo para casos avanzados

Creado: 2025-11-02 Actualizado: 2025-11-02 Responsable: Equipo Backend