Diseño Code-First

Standards Jan 9, 2026 JAVA
code-first api-design design-methodology implementation-first annotations

Definición

El diseño code-first es un enfoque de desarrollo de APIs donde escribes el código de implementación primero, usando anotaciones o comentarios específicos del framework para describir el comportamiento de la API, luego generas automáticamente especificaciones (como OpenAPI), documentación y SDKs cliente desde el código. Esto contrasta con contract-first donde defines la especificación antes de implementar.

La idea central es que tu código fuente es la fuente única de verdad. Anotaciones como @Path("/users"), @ApiResponse, o metadata de decoradores describen endpoints, esquemas y reglas de validación directamente en el código. Herramientas en tiempo de construcción o ejecución escanean la base de código, extraen estas anotaciones y generan especificaciones OpenAPI, documentación de Swagger UI y definiciones de tipos.

Code-first atrae a desarrolladores que prefieren trabajar en su lenguaje de programación en lugar de YAML/JSON. Promete “escribe código una vez, obtén documentación gratis”. Muchos frameworks populares (Spring Boot, ASP.NET Core, FastAPI, NestJS) tienen excelente herramientas code-first con anotaciones que generan especificaciones automáticamente. Sin embargo, intercambia beneficios de fase de diseño por conveniencia de implementación.

Ejemplo

API REST Spring Boot: Un desarrollador Java anota clases de controlador con @RestController, @GetMapping, @PostMapping, y usa @ApiOperation de Springdoc. Cuando la aplicación se ejecuta, Springdoc genera una especificación OpenAPI en /v3/api-docs y sirve Swagger UI en /swagger-ui.html - todo derivado automáticamente desde anotaciones del código.

FastAPI en Python: Los desarrolladores Python usan hints de tipo y modelos Pydantic para definir endpoints. FastAPI introspecciona estos tipos en tiempo de ejecución y auto-genera especificaciones OpenAPI, documentación interactiva y validación de JSON Schema - sin escritura manual de especificaciones requerida.

ASP.NET Core: Los desarrolladores C# decoran controladores con atributos como [HttpGet], [ProducesResponseType], y comentarios XML. Swashbuckle analiza estos en tiempo de construcción, generando especificaciones OpenAPI que alimentan documentación de API y generación de SDKs cliente.

NestJS TypeScript: Los desarrolladores usan decoradores como @Get(), @Body(), @ApiProperty() en DTOs. El módulo Swagger de NestJS refleja en estos decoradores para generar especificaciones OpenAPI que coinciden con la implementación real.

Prototipado Rápido: Una startup construye rápidamente un MVP escribiendo endpoints de Flask o Express. Una vez que las rutas funcionan, agregan anotaciones mínimas y generan especificaciones OpenAPI para equipos de frontend, evitando diseño de especificación inicial cuando los requisitos no están claros.

Analogía

Escribir Primero, Esquema Después: Code-first es como escribir un ensayo redactando inmediatamente párrafos, luego usando una herramienta para extraer encabezados y generar una tabla de contenidos desde el texto. Contract-first es como esquematizar la estructura del ensayo primero, obtener feedback sobre el esquema, luego escribir párrafos para llenar cada sección.

Música Grabada: Code-first es como una banda improvisando una canción en el estudio, grabándola, luego alguien la transcribe a partitura. Contract-first es como escribir la partitura primero, revisarla con la banda, luego grabar exactamente lo que fue escrito.

Construcción Sin Planos: Code-first es como constructores habilidosos construyendo un cobertizo basándose en experiencia e intuición, luego teniendo un arquitecto que cree dibujos as-built documentando lo que fue construido. Contract-first es el enfoque tradicional - el arquitecto dibuja planos primero, los constructores los siguen precisamente.

Ejemplo de Código

Spring Boot (Java) - Code-First con Anotaciones


import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.responses.*;
import javax.validation.Valid;
import javax.validation.constraints.*;

@RestController
@RequestMapping("/api/users")
@Tag(name = "Users", description = "Endpoints de gestión de usuarios")
public class UserController {

    @GetMapping("/{id}")
    @Operation(summary = "Obtener usuario por ID", description = "Devuelve un solo usuario")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "Usuario encontrado",
            content = @Content(schema = @Schema(implementation = User.class))),
        @ApiResponse(responseCode = "404", description = "Usuario no encontrado",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    })
    public User getUserById(@PathVariable String id) {
        return userService.findById(id);
    }

    @PostMapping
    @Operation(summary = "Crear un nuevo usuario")
    @ApiResponse(responseCode = "201", description = "Usuario creado",
        content = @Content(schema = @Schema(implementation = User.class)))
    @ApiResponse(responseCode = "400", description = "Entrada inválida")
    public User createUser(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }
}

@Schema(description = "Entidad de usuario")
class User {
    @Schema(description = "Identificador único de usuario", example = "123e4567-e89b-12d3-a456-426614174000")
    private String id;

    @Schema(description = "Dirección de email del usuario", example = "[email protected]")
    @Email
    private String email;

    @Schema(description = "Nombre para mostrar del usuario", example = "Alice Smith")
    @NotBlank
    @Size(min = 3, max = 50)
    private String name;

    // getters, setters
}

class CreateUserRequest {
    @Email
    @NotBlank
    private String email;

    @NotBlank
    @Size(min = 3, max = 50)
    private String name;

    @Size(min = 12, max = 128)
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*?&]{12,}$")
    private String password;

    // getters, setters
}

class ErrorResponse {
    private String code;
    private String message;
    // getters, setters
}

Especificación OpenAPI generada (automática):


# Esto es auto-generado por Springdoc en /v3/api-docs
openapi: 3.0.1
info:
  title: User API
  version: 1.0.0
paths:
  /api/users/{id}:
    get:
      tags:
        - Users
      summary: Obtener usuario por ID
      description: Devuelve un solo usuario
      operationId: getUserById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Usuario encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: Usuario no encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/users:
    post:
      tags:
        - Users
      summary: Crear un nuevo usuario
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: Usuario creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Entrada inválida

components:
  schemas:
    User:
      type: object
      description: Entidad de usuario
      properties:
        id:
          type: string
          description: Identificador único de usuario
          example: 123e4567-e89b-12d3-a456-426614174000
        email:
          type: string
          format: email
          description: Dirección de email del usuario
          example: [email protected]
        name:
          type: string
          minLength: 3
          maxLength: 50
          description: Nombre para mostrar del usuario
          example: Alice Smith

    CreateUserRequest:
      type: object
      required: [email, name, password]
      properties:
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 3
          maxLength: 50
        password:
          type: string
          minLength: 12
          maxLength: 128
          pattern: "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*?&]{12,}$"

    ErrorResponse:
      type: object
      properties:
        code:
          type: string
        message:
          type: string

FastAPI (Python) - Code-First con Type Hints


from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

app = FastAPI(title="User API", version="1.0.0")

# Los modelos Pydantic definen esquemas automáticamente
class CreateUserRequest(BaseModel):
    email: EmailStr
    name: str = Field(..., min_length=3, max_length=50)
    password: str = Field(..., min_length=12, max_length=128)

class User(BaseModel):
    id: str = Field(..., description="Identificador único de usuario")
    email: EmailStr = Field(..., description="Dirección de email del usuario")
    name: str = Field(..., description="Nombre para mostrar del usuario")
    created_at: datetime

    class Config:
        schema_extra = {
            "example": {
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "email": "[email protected]",
                "name": "Alice Smith",
                "created_at": "2024-01-01T10:00:00Z"
            }
        }

@app.get("/api/users/{user_id}",
         response_model=User,
         summary="Obtener usuario por ID",
         responses={404: {"description": "Usuario no encontrado"}})
async def get_user(user_id: str):
    """Devuelve un solo usuario por ID"""
    user = await user_service.find_by_id(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="Usuario no encontrado")
    return user

@app.post("/api/users",
          response_model=User,
          status_code=201,
          summary="Crear un nuevo usuario")
async def create_user(request: CreateUserRequest):
    """Crea una nueva cuenta de usuario"""
    return await user_service.create(request)

# FastAPI auto-genera OpenAPI en /openapi.json
# Sirve documentación interactiva en /docs (Swagger UI)
# Sirve documentación alternativa en /redoc (ReDoc)

Diagrama

graph TB
    subgraph "Flujo Code-First"
        START[Escribir Implementación
Con Anotaciones] START --> BUILD[Proceso de
Build/Runtime] BUILD --> EXTRACT[Extraer Anotaciones
Reflection/AST] EXTRACT --> GEN_SPEC[Generar Especificación OpenAPI
Automático] GEN_SPEC --> DOCS[Swagger UI
Documentación] GEN_SPEC --> SDK[Generar SDKs
Desde Especificación] GEN_SPEC --> VALIDATE[Validación
Desde Especificación] subgraph "Desafíos" DRIFT[Riesgo de Divergencia
Si anotaciones incompletas] DESIGN[Sin Fase de Diseño
Revisión de Interesados] REFACTOR[Impacto de Refactoring
Rompe API] end GEN_SPEC -.Riesgos.-> DRIFT START -.Riesgos.-> DESIGN START -.Riesgos.-> REFACTOR end style START fill:#87CEEB style GEN_SPEC fill:#90EE90 style DRIFT fill:#FFB6C6 style DESIGN fill:#FFB6C6

Buenas Prácticas

  1. Anota exhaustivamente - No confíes en valores por defecto, anota explícitamente respuestas, errores, ejemplos
  2. Versiona desde el inicio - Incluso code-first se beneficia de versionado explícito (ej., /v1/users)
  3. Valida la especificación generada - Revisa la OpenAPI auto-generada para completitud y precisión
  4. Exporta y versiona especificaciones - Haz commit de las especificaciones generadas a git para rastrear evolución de la API
  5. Usa validación del framework - Deja que las anotaciones del framework manejen la validación de entrada (ej., @Valid)
  6. Documenta ejemplos en código - Usa características de anotación para proveer ejemplos realistas
  7. Combina con pruebas de contrato - Genera especificación, luego prueba implementación contra ella
  8. Considera enfoque híbrido - Comienza code-first para prototipar, cambia a contract-first para producción

Errores Comunes

Anotaciones incompletas: Confiar en valores por defecto del framework sin anotaciones explícitas para respuestas de error, autenticación o ejemplos. Las especificaciones generadas tienen huecos.

Ignorar especificación generada: Auto-generar la especificación pero nunca revisarla. Los clientes podrían obtener documentación pobre o esquemas incorrectos.

Sin versionado de especificación: Generar especificaciones al vuelo sin hacerles commit a control de versiones. Sin historial de cambios de API, difícil de detectar cambios incompatibles.

El refactoring rompe la API: Renombrar un campo de clase rompe el esquema de respuesta de la API sin advertencia. Code-first carece de sistemas de alerta temprana.

Lock-in del framework: Uso intensivo de anotaciones específicas del framework hace la migración difícil. La especificación no es agnóstica del framework.

Falta revisión de diseño: Saltar revisión de interesados del diseño de API porque “el código es la especificación”. Fallas de diseño descubiertas tarde.

Sobrecarga de anotaciones: El código se vuelve desordenado con tantas anotaciones de documentación que es difícil leer la lógica de negocio.

Standards & RFCs

Standards & RFCs
1)- [OpenAPI 3](https://reference.apios.info/es/terms/openapi-3/).1.0 Specification
2)- JSON Schema Draft 2020-12
3)- JSR 380 - Bean Validation 2.0 (Java)
4)- Swagger Annotations (Java)
5)- Pydantic (Python type validation)

Términos Relacionados