Diseno de APIs para sistemas que sobreviven a tu equipo
Como disenar APIs que sobrevivan a la rotacion de equipos. Protobuf como fuente de verdad, tRPC para stacks TypeScript, GraphQL en produccion, gestion de breaking changes y contratos de error.
La API que sobrevive a su creador
Cada API empieza con un equipo, un cliente y un caso de uso. Dos anos despues, 5 equipos dependen de ella, 3 socios externos se integran con ella, y los desarrolladores originales se fueron. Las decisiones de diseno de la API ahora son permanentes.
Hemos construido APIs con tres estrategias diferentes: Protobuf como fuente de verdad del schema para sistemas multi-lenguaje, tRPC para stacks 100% TypeScript, y GraphQL para necesidades complejas del cliente. Cada una fue la eleccion correcta en su contexto. Este articulo cubre cuando usar cada una y como disenar APIs que sobrevivan a la rotacion de equipos.
Para entender como estas APIs encajan en un diseno de sistema mas amplio, consulta nuestra guia de arquitectura de sistemas y nuestra guia de ingenieria de software.
Protobuf: fuente de verdad para sistemas multi-lenguaje
Cuando tu sistema abarca multiples lenguajes (API TypeScript, servicio de busqueda en Go, pipeline ML en Python), necesitas un formato de schema que genere tipos para todos ellos. Protobuf hace exactamente eso.
// proto/product.proto
syntax = "proto3";
package commerce.v1;
message Product {
string id = 1;
string name = 2;
string description = 3;
int32 price_cents = 4;
string currency = 5;
repeated string category_ids = 6;
ProductStatus status = 7;
google.protobuf.Timestamp created_at = 8;
}
enum ProductStatus {
PRODUCT_STATUS_UNSPECIFIED = 0;
PRODUCT_STATUS_DRAFT = 1;
PRODUCT_STATUS_ACTIVE = 2;
PRODUCT_STATUS_ARCHIVED = 3;
}
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product);
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
rpc CreateProduct(CreateProductRequest) returns (Product);
}
Por que los tags importan para la evolucion
Los campos de Protobuf se identifican por numeros de tag (1, 2, 3...), no por nombre. Esto significa que puedes renombrar un campo sin romper clientes existentes. Puedes agregar nuevos campos (nuevos numeros de tag) sin romper clientes antiguos. Puedes depreciar campos sin eliminarlos.
// Evolucion segura: agregar un campo
message Product {
string id = 1;
string name = 2;
string description = 3;
int32 price_cents = 4;
string currency = 5;
repeated string category_ids = 6;
ProductStatus status = 7;
google.protobuf.Timestamp created_at = 8;
string sku = 9; // NUEVO: agregado sin romper clientes existentes
string brand = 10; // NUEVO: los clientes antiguos simplemente ignoran campos desconocidos
}
Reglas para una evolucion segura de Protobuf:
- Nunca reutilices un numero de tag (ni siquiera despues de eliminar un campo)
- Nunca cambies el tipo de un campo
- Agrega nuevos campos con nuevos numeros de tag
- Marca los campos depreciados con
reservedpara evitar la reutilizacion accidental
Generacion de codigo
# Genera TypeScript, Go y Python desde el mismo archivo .proto
protoc --ts_out=./gen/ts --go_out=./gen/go --python_out=./gen/py proto/*.proto
Un solo archivo de schema genera clientes y servidores type-safe en cada lenguaje. Los cambios de schema son un pull request. La generacion de codigo es un paso de CI. Las incompatibilidades de tipos son errores de compilacion, no bugs en runtime.
tRPC: cuando todo tu stack es TypeScript
Si tu servidor API y todos tus clientes son TypeScript, tRPC elimina la capa de schema por completo. Los tipos fluyen del servidor al cliente en tiempo de compilacion. Sin generacion de codigo, sin archivos de schema, sin spec OpenAPI.
// Servidor: definicion del router con procedimientos tipados
import { router, publicProcedure, protectedProcedure } from './trpc';
import { z } from 'zod';
export const productRouter = router({
list: publicProcedure
.input(z.object({
cursor: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
category: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
return ctx.productService.list(input);
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
description: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
return ctx.productService.create(ctx.tenantId, input);
}),
});
// Cliente: inferencia de tipos completa, sin generacion de codigo
const products = await trpc.product.list.query({
limit: 10,
category: 'electronics',
});
// products esta completamente tipado: { items: Product[], nextCursor?: string }
Cuando tRPC funciona mejor
- Todos los clientes son TypeScript (app web, React Native, servicios Node.js)
- La API es interna (no expuesta a terceros)
- La iteracion rapida importa mas que los contratos formales de API
- El equipo es pequeno y co-localizado (los cambios de servidor y cliente se hacen juntos)
Cuando tRPC no funciona
- Socios externos necesitan integrarse (necesitan documentacion OpenAPI/Swagger)
- Los clientes estan en otros lenguajes (mobile nativo, Go, Python)
- Necesitas versionado de API para retrocompatibilidad
- La API es un producto publico
Usamos tRPC con Hono para APIs TypeScript internas. Consulta nuestra guia de backends TypeScript para la comparacion completa de stacks.
GraphQL: para necesidades complejas del cliente
GraphQL brilla cuando los clientes necesitan queries flexibles: diferentes paginas necesitan diferentes subconjuntos de datos, mobile necesita menos datos que web, y el equipo frontend quiere iterar en queries sin cambios en el backend.
# El cliente solicita exactamente lo que necesita
query ProductPage($slug: String!) {
product(slug: $slug) {
id
name
price
images { url alt }
reviews(first: 5, status: APPROVED) {
items { rating body customerName }
totalItems
}
relatedProducts(first: 4) {
id name price images { url }
}
}
}
Patrones de GraphQL en produccion
Queries persistidos: No permitas queries arbitrarios en produccion. Los clientes envian un hash del query, el servidor busca el query en un registro. Esto previene abusos de queries y habilita el caching.
Limitacion de profundidad: Sin limites, un cliente puede enviar un query profundamente anidado que hace joins con todas las tablas de tu base de datos.
apiOptions: {
middleware: [depthLimit(10)],
shopApiPlayground: false, // Desactivado en produccion
}
Prevencion del N+1: Usa DataLoader para agrupar consultas a la base de datos. Sin esto, un query que obtiene 20 productos con sus categorias hace 20 consultas de categorias separadas.
// DataLoader agrupa N consultas individuales en 1
const categoryLoader = new DataLoader(async (ids: string[]) => {
const categories = await categoryRepo.findByIds(ids);
return ids.map(id => categories.find(c => c.id === id));
});
Analisis de complejidad: Asigna un costo a cada campo. Rechaza queries que excedan un presupuesto de complejidad.
Para ver como usamos GraphQL en Vendure commerce, consulta nuestra guia de produccion de Vendure.
Breaking changes: evolucion por tags vs versionado por URL
| Estrategia | Como funciona | Ideal para |
|---|---|---|
| Por tags (Protobuf) | Agrega campos con nuevos tags, los clientes antiguos los ignoran | Multi-lenguaje, gRPC |
Versionado por URL (/v1/, /v2/) | Endpoints separados por version | APIs REST con consumidores externos |
Versionado por header (Accept: application/vnd.api.v2+json) | Misma URL, version en el header | APIs REST con URLs limpias |
| GraphQL (sin versionado) | Agrega campos, deprecia los antiguos con @deprecated | APIs GraphQL |
| tRPC (sin versionado) | Los tipos evolucionan con la codebase | APIs TypeScript internas |
Para la mayoria de las APIs internas, evita el versionado por URL. Duplica tu superficie de mantenimiento. Agrega nuevos campos, deprecia los antiguos, eliminanos una vez que todos los clientes hayan migrado.
Para APIs externas (integraciones de terceros, APIs publicas), el versionado por URL es mas seguro porque no controlas cuando los clientes actualizan.
Contratos de error
Los errores son parte del contrato de la API. Los clientes necesitan errores estructurados sobre los que puedan actuar, no mensajes de texto que parsean con regex.
// Respuesta de error estructurada
interface ApiError {
code: string; // Legible por maquina: "PRODUCT_NOT_FOUND", "INSUFFICIENT_STOCK"
message: string; // Legible por humanos: "Product with ID xyz not found"
details?: object; // Contexto adicional para debugging
requestId: string; // ID de correlacion para soporte
}
// Ejemplos de respuestas
// 404
{
"code": "PRODUCT_NOT_FOUND",
"message": "Product with ID prod_123 not found",
"requestId": "req_abc456"
}
// 409
{
"code": "INSUFFICIENT_STOCK",
"message": "Only 2 units available, requested 5",
"details": { "available": 2, "requested": 5 },
"requestId": "req_def789"
}
// 422
{
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": {
"fields": [
{ "field": "price", "error": "must be positive" },
{ "field": "name", "error": "must not be empty" }
]
},
"requestId": "req_ghi012"
}
El campo code es el contrato. Los clientes hacen switch sobre el. El message es para humanos. Los details proporcionan contexto. El requestId permite al soporte encontrar la solicitud exacta en los logs.
Gobernanza de API
A medida que la API crece, necesitas gobernanza para prevenir inconsistencias:
| Regla | Por que |
|---|---|
| Todos los endpoints requieren autenticacion | Sin endpoints publicos accidentales |
| Todas las mutaciones requieren claves de idempotencia | Los reintentos de red no crean duplicados |
Todas las respuestas incluyen requestId | Cada solicitud es rastreable |
| Todos los errores usan el formato estructurado | Los clientes pueden manejar errores de forma programatica |
| Todos los endpoints de lista soportan paginacion | Sin consultas no acotadas |
| Los breaking changes requieren review | Una sola persona no puede romper todos los consumidores |
| Las depreciaciones requieren un calendario de migracion | "Depreciado" sin fecha limite significa "nunca eliminado" |
Errores comunes
-
REST vs GraphQL como identidad. Elige segun las necesidades del cliente, no la preferencia del equipo. REST para CRUD simple con consumidores externos. GraphQL para queries flexibles con equipos frontend. tRPC para TypeScript interno. Protobuf para multi-lenguaje.
-
Sin contrato de error. Mensajes de error de texto que cambian con cada release rompen cada cliente que intenta manejarlos.
-
Endpoints de lista no acotados. Un endpoint que devuelve los 50,000 productos en una sola respuesta va a crashear clientes y sobrecargar tu base de datos. La paginacion es obligatoria.
-
Versionar APIs internas. Si controlas todos los clientes, haz evolucionar la API in situ. El versionado duplica el mantenimiento sin beneficio.
-
Sin calendario de depreciacion. Marcar un campo
@deprecatedsin fecha de eliminacion significa que se queda para siempre. Fija una fecha, notifica a los consumidores, eliminalo. -
GraphQL sin limites de profundidad. Una API GraphQL sin restricciones es un vector de denegacion de servicio. Limita la profundidad, la complejidad y el costo de los queries.
Puntos clave
-
Protobuf para sistemas multi-lenguaje. Evolucion por tags, generacion de codigo, eficiencia del formato wire. El schema es el contrato.
-
tRPC para stacks 100% TypeScript. Cero generacion de codigo, inferencia de tipos completa, maxima velocidad de iteracion. Pero solo funciona cuando todos los clientes son TypeScript.
-
GraphQL para necesidades flexibles del cliente. Los clientes consultan exactamente lo que necesitan. Pero agrega limites de profundidad, queries persistidos y analisis de complejidad para produccion.
-
Los contratos de error son tan importantes como los contratos de exito. Codigos legibles por maquina, mensajes legibles por humanos, IDs de correlacion y detalles estructurados.
-
Evoluciona in situ para APIs internas, versiona para externas. Agregar campos es seguro. Eliminar campos requiere un calendario de migracion. El versionado por URL es el ultimo recurso.
Disenamos APIs como parte de nuestras practicas de desarrollo web y software a medida. Si necesitas ayuda con la arquitectura de tus APIs, habla con nuestro equipo o solicita un presupuesto.
Temas cubiertos
Guías relacionadas
Guía Empresarial de Sistemas de IA Agéntica
Guia tecnica de sistemas de IA agentica en entornos empresariales. Descubre la arquitectura, capacidades y aplicaciones de agentes IA autonomos.
Leer guíaComercio Agéntico: Cómo Dejar que los Agentes IA Compren de Forma Segura
Cómo diseñar comercio iniciado por agentes IA con gobernanza. Motores de políticas, puertas de aprobación HITL, recibos HMAC, idempotencia, aislamiento de tenants y el Agentic Checkout Protocol completo.
Leer guíaLos 9 Puntos Donde Tu Sistema de IA Filtra Datos (y Cómo Sellar Cada Uno)
Un mapa sistemático de cada lugar donde se filtran datos en sistemas de IA. Prompts, embeddings, logs, llamadas a herramientas, memoria de agentes, mensajes de error, caché, datos de fine-tuning y handoffs entre agentes.
Leer guía¿Listo para construir sistemas de IA listos para producción?
Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.
Iniciar una conversación