El enrutador es el padrón
Hanzo Cloud no guarda un archivo OpenAPI en el repositorio. La tabla viva de rutas es la fuente, proyectada de tres formas, y una prueba de biyección hace fallar el build si alguna proyección se corre. 1,362 operaciones en 963 rutas, generadas por proceso al momento del pedido.

En esta página
En el repositorio de Hanzo Cloud no hay un openapi.yaml. No hay un segundo padrón que liste endpoints, ni un archivo de anotaciones, ni un paso de generador en CI que escriba una spec a disco. Si quieren saber qué es la API, le preguntan a un proceso que está corriendo, y se los dice leyendo su propio enrutador.
Este post cuenta por qué ese terminó siendo el único arreglo que se sostiene, y también las partes que no caminan, que resultan la mitad más útil.
Una tabla, tres proyecciones
serve.go arma una sola tabla de rutas. Después la leen tres cosas, todas luego de MountAll, para que cada una vea la tabla completa:
/zaprepite los handlers de/v1sobre el transporte ZAP.- La consola los dibuja.
GET /v1/openapi.jsonlos describe.
Ninguna de las tres se queda con una copia de nada. Esa es toda la restricción de diseño, y el resto sale de ahí. Una proyección que guardara su propia lista de endpoints sería una segunda fuente de verdad, y una segunda fuente de verdad se corre: no de vez en cuando, siempre, al ritmo con el que alguien se olvida.
La proyección que describe es la que la gente pregunta. Entonces: openapi.Live(app) llama a app.Fiber().GetRoutes(true), y toda otra función de openapi/ es una función pura del []Route que regresa. El documento no se construye desde el enrutador. El documento es el enrutador, con otra forma.
Por qué leer el enrutador vivo es la única fuente total
La alternativa obvia es escanear el fuente. No sirve, y vale la pena decir con precisión por qué, porque es la razón de que un montón de herramientas de API reporten de menos, sin hacer ruido.
POST /v1/kms/auth/login se registra así:
Group("/v1/kms/auth").Post("/login")
Ningún grep encuentra ese path. El string /v1/kms/auth/login no está en el código. Existe solo en el enrutador ya armado, en runtime, después de que el framework concatena el prefijo del grupo y la hoja.
Después se pone peor, de una forma que en realidad está bien. El conjunto de rutas es una función de la configuración del deploy: cfg.Enabled, más las compuertas internas como el if kc != nil de KMS. Un deploy que no monta admin no sirve rutas de admin. Entonces la spec honesta de ese deploy no las publica.
Por eso el documento se genera por proceso, al momento del request, y no se arma una sola vez en CI. Una spec armada en CI describe un deploy máximo hipotético, que puede no ser el que ustedes están hablando. Una spec leída del enrutador vivo describe el proceso que les contestó.
La prueba que lo vuelve cierto
Una arquitectura donde la spec no se puede correr es una afirmación, y las afirmaciones necesitan una compuerta. La nuestra es una biyección, en cmd/cloud/openapi_test.go:
func TestSpecDoesNotDriftFromLiveRouter(t *testing.T) {
Monta cada subsistema en apps.Wire(), genera el documento y afirma el mapeo para los dos lados: cada ruta viva aparece como operación, y cada operación tiene detrás una ruta viva. No un subconjunto en ninguna dirección.
Corrido hoy:
bijection holds over 1362 operations / 963 paths / 138 products
Es la única prueba del repositorio cuyo fallo quiere decir que el documento publicado miente. Todo lo demás que se rompe deja mal el producto; esta deja mal el contrato, y eso es peor, porque quien integra ya construyó contra él.
Que el conteo se mueva no es un problema: se mueve cada vez que se suma un subsistema. El número que importa es la diferencia entre los dos lados, y ese número es cero o el build falla.
El eje de producto es mecánico
El primer segmento del path después de /v1/ es el producto. openapi.Product lo lee y con eso etiqueta cada operación, que es lo que le permite a una CLI armar hanzo <product> <resource> <verb> sin que nadie opine dónde debería vivir un comando.
A propósito, no es el nombre del subsistema. clients/billing también sirve /v1/finance/*, y la URL es lo que ve quien llama, así que gana la URL. Nombrar el eje según el paquete interno habría vuelto la CLI un artefacto curado —una cosa con opiniones adentro, que alguien tiene que mantener— en vez de una proyección.
Lo que el enrutador no les puede decir
Acá está el límite, y no pensamos esquivarlo.
Método, path, parámetros de path y producto se derivan del enrutador. Los esquemas de request y de response, los parámetros de query y de header, los códigos de estado y la auth no. El enrutador guarda un func(*zip.Ctx) error. El tipo del request es una variable local dentro del cuerpo del handler:
var req secretPutRequest
json.Unmarshal(ctx.Body(), &req)
Go no puede reflejar desde un valor de función hacia su cuerpo. Acá no hay un truco, y probar uno en el generador sería adivinar, que es la forma en que una spec junta entradas seguras y equivocadas.
El único camino a los esquemas es una op tipada:
zip.Get[In, Out](...)
In y Out están en el registro, así que el esquema está disponible sin leerle la mente a nadie. Y, como están en el registro, la misma entrada también deja una herramienta MCP: zip/openapi.go y zip/mcp.go son dos lectores de un solo padrón. Esa es la cuarta proyección, y llegó sin un cuarto mecanismo.
La migración es incremental por cómo está armada: GetRoutes() es un superconjunto de app.ops, así que pasar un handler a una op tipada suma esquema sin tocar nada que esté aguas arriba.
Las rutas que no se pueden tipar, y por qué
clients/integrations está convertido hasta donde el wire lo deja: 20 de 41 rutas son ops tipadas. Las otras 21 siguen como handlers crudos, y cada una está en un mapa rawRoutes de ops_projection_test.go con la razón por la que no se puede mover:
- 13 redirects del navegador. El callback de OAuth y cada tramo del enlace contestan
302. Una op tipada devuelve un valor, no un headerLocationy un estado en los 300. - 6 webhooks. HMAC de Slack y de GitHub, Ed25519 de Discord, JWT del Bot Framework de Teams, el header secret-token de Telegram. Cada uno autentica una firma sobre los bytes crudos del request. Una op tipada recibe un
Inya decodificado y nunca ve los bytes sobre los que se calculó la firma, así que tiparlos les rompería la auth. - 2 creadores de
202 Accepted.cloud.Creatednos da una junta de 201. No hay junta de 202. Dos rutas necesitan una, así que dos rutas esperan.
Para nosotros esa lista es el artefacto que más vale en todo el sistema. Es un inventario enumerado, clavado por pruebas, de exactamente dónde se corta la abstracción, con una razón por entrada que un revisor puede discutir. El fallo que evita es el de una ruta que, en silencio, vuelve a un handler crudo y desaparece junta del documento, del set de herramientas MCP y de la CLI, sin que se note, porque nada afirmaba que había estado ahí.
Una arquitectura no son las partes que generalizan. Son esas partes, más un recuento honesto y exigido de las partes que no.
Qué se gana con esto
Quien integra contra Hanzo Cloud lee un documento que produjo el proceso que le está contestando los requests, en el deploy con el que de verdad habla, hace unos milisegundos. Un agente que llama por MCP llega al mismo handler, con el mismo chequeo de tenant, que quien llama por REST: una llamada anónima a /mcp recibe el mismo 403 que da REST, porque es el mismo camino de código y una prueba lo fija.
Nada de eso pidió que alguien se acordara de regenerar algo.
La forma general: si dos artefactos tienen que coincidir, no generen uno a partir del otro y esperen. Hagan que uno sea una vista del otro, y escriban la prueba que falla cuando no lo son.
Leer más

Cloud-Native Commerce: Why Architecture Matters
How we built Crowdstart as a cloud-native platform from day one, and why this matters for commerce.

Comercio headless listo para compradores que son agentes
Los compradores se están volviendo agentes. Qué hace falta para que un LLM pueda leer, firmar y pagar sin una persona de por medio, y qué entrega Hanzo hoy para eso.

Introducing Enso — one API, routed to the best frontier intelligence
Enso is Hanzo's learned router: no single frontier model leads every benchmark, so Enso routes each request to the model most likely to win it — and learns from your feedback as it goes. Microsecond routing on a CPU, a transparent per-request meter, and a 1% fee that pays for itself. Here is how it works, the measured results, and the cost-transparency angle a monolith cannot offer.