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