Modelado de requests y responses
Una API durable no se rompe solo por cambiar endpoints. Muchas veces se rompe por decisiones pequeñas en el shape del request o de la response.
Diseñar una API es modelar intercambios:
- qué entra
- qué sale
- qué errores viven al costado
- qué partes pueden crecer sin romper consumidores
La analogía: la comanda bien escrita
Una comanda mala en un restaurante genera caos:
- platillos ambiguos
- cantidades poco claras
- notas perdidas
- errores imposibles de rastrear
Una comanda buena separa campos:
mesa: 7
platillo: tacos_al_pastor
cantidad: 2
sin_cebolla: true
bebida: agua
Eso mismo buscamos en un request.
Resources vs RPC
Dos estilos comunes:
Resource-oriented
GET /v1/conversations/42
POST /v1/conversations
GET /v1/fine-tuning/jobs/abc
Ventaja:
- se lee como modelo de dominio
- se integra bien con HTTP
RPC-style
POST /v1/chat:generate
POST /v1/model:embed
POST /v1/fine-tuning:start
Ventaja:
- más natural cuando la operación es acción pura
Regla práctica:
- si modelas entidades estables, piensa en recursos
- si modelas una operación computacional fuerte, RPC puede ser más claro
El costo de mezclar sin criterio
Mezclar recursos y RPC no es un pecado. El problema es mezclar sin convención.
Ejemplo confuso:
GET /v1/conversations/42
POST /v1/sendMessage
POST /v1/jobs/startFineTune
GET /v1/getUsage
Aquí el consumer tiene que aprender cuatro estilos distintos a la vez.
Regla práctica:
- puedes usar un diseño híbrido
- pero cada familia de endpoints debe seguir una lógica clara
- la inconsistencia cuesta más a medida que crecen los consumers
Paginación
Colecciones grandes necesitan frontera.
Malo:
GET /v1/logs
Respuesta:
- 100,000 elementos
- lenta
- cara
- difícil de consumir
Mejor:
GET /v1/logs?limit=50&cursor=abc123
Response:
{
"data": [...],
"next_cursor": "def456"
}
Offset vs cursor
| Estrategia | Ventaja | Riesgo |
|---|---|---|
offset |
simple de entender | inestable cuando cambian los datos |
cursor |
más estable para feeds grandes | menos intuitivo para humanos |
Por qué offset puede salir mal
Imagina este flujo:
- pides
GET /logs?offset=0&limit=3 - recibes
[A, B, C] - entra un log nuevo al inicio de la colección
- pides
GET /logs?offset=3&limit=3
Ahora podrías:
- repetir un elemento
- saltarte uno
Con cursor, en cambio, la página siguiente se ancla a una posición lógica del feed, no a un número absoluto fácilmente desplazable.
Filtering y sorting
Los filtros deben ser predecibles y composables.
GET /v1/fine-tuning/jobs?status=running&sort=-created_at
Reglas sanas:
- usa nombres explícitos
- evita semánticas mágicas
- documenta enums válidos
Error envelopes
Si cada endpoint falla con un shape distinto, el consumer vive en caos.
Mejor:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Demasiadas peticiones por minuto",
"retry_after_seconds": 30
}
}
Eso permite:
- UI consistente
- logging consistente
- retries mejor informados
Idempotencia
Hay operaciones que deben poder repetirse sin crear efectos dobles.
Ejemplo clásico:
POST /v1/payments
Idempotency-Key: 8f4e...
En un sistema LLM, un caso parecido podría ser:
- crear un job de fine-tuning
- registrar una compra de créditos
Si el cliente reintenta por timeout, no quieres dos jobs ni dos cobros.
Qué pasa si la ignoras
Sin idempotencia, un simple retry automático puede producir:
- dos entrenamientos idénticos
- dos compras de créditos
- dos correos o notificaciones
El bug no viene de “hacer dos clicks”. Viene de no diseñar la operación para un mundo con retries, timeouts y redes imperfectas.
Forma durable de una response
Malo:
{
"text": "Hola"
}
Tal vez hoy basta. Pero mañana quizá quieras:
- metadata
- usage
- finish reason
- trace id
Más durable:
{
"id": "msg_001",
"output_text": "Hola",
"usage": {
"input_tokens": 10,
"output_tokens": 12
},
"meta": {
"model": "chat-mini",
"trace_id": "tr_123"
}
}
La idea no es inflar responses. Es dejar espacio para crecer con coherencia.
Additive vs breaking
Al modelar shapes, piensa desde el inicio qué cambios suelen ser seguros:
Normalmente aditivo
- agregar campo opcional
- agregar nuevo status code documentado
- agregar valor de enum si el consumer lo tolera
Normalmente breaking
- renombrar campo
- cambiar tipo
- quitar campo usado
- cambiar semántica silenciosamente
La condición que hace seguro lo aditivo
Decir que “agregar un campo opcional no rompe” solo es razonable si el consumer está diseñado como tolerant reader:
- lee lo que conoce
- ignora campos desconocidos
Si el consumer valida de forma rígida o asume que la response tiene exactamente N campos, incluso un cambio aditivo puede romperlo.
Producer sano: puede agregar sin borrar
Consumer sano: tolera campos nuevos
Esa combinación es la base práctica de mucha compatibilidad hacia adelante.
Conexión con la arquitectura del LLM
Para el chatbot, estos objetos suelen merecer diseño cuidadoso:
ChatRequestChatResponseConversationFineTuningJobApiError
Si esos shapes están bien modelados, los consumers cambian menos y los tests detectan mejor cualquier drift.
Verifica en el notebook: Revisa
clase/19_diseno_api/code/02_modelado_y_versionado.ipynbpara comparar responses frágiles vs durables, y ver qué cambios son aditivos y cuáles rompen clientes.
Supón que hoy tu backend devuelve:
{
"reply": "Hola",
"tokens": 22
}
Diseña una versión más durable que permita crecer hacia:
- múltiples outputs
- metadata del modelo
- errores consistentes
- trazabilidad
Explica qué parte de tu rediseño busca claridad humana y qué parte busca compatibilidad futura.