Le routeur est le registre
Hanzo Cloud n'a aucun fichier OpenAPI versionné. La table de routes vivante est la source, projetée de trois façons, et un test de bijection fait échouer la compilation si une projection dérive. 1,362 opérations sur 963 paths, générées par processus au moment de la requête.

Sur cette page
Il n'y a pas de openapi.yaml dans le dépôt de Hanzo Cloud. Il n'y a pas de second registre qui liste les endpoints, pas de fichier d'annotations, et pas d'étape de générateur dans la CI qui écrirait une spécification sur disque. Si vous voulez savoir ce qu'est l'API, vous interrogez un processus en cours d'exécution, et il vous répond en lisant son propre routeur.
Ce texte explique pourquoi cet arrangement a fini par être le seul qui tienne, et aussi les parties qui ne marchent pas — lesquelles se révèlent la moitié la plus utile.
Une table, trois projections
serve.go compose une seule table de routes. Trois choses la lisent ensuite, toutes après MountAll, de sorte que chacune voit une table complète :
/zaprejoue les handlers de/v1sur le transport ZAP.- La console les dessine.
GET /v1/openapi.jsonles décrit.
Aucune des trois ne détient une copie de quoi que ce soit. C'est toute la contrainte de conception, et le reste en découle. Une projection qui garderait sa propre liste d'endpoints serait une seconde source de vérité, et une seconde source de vérité dérive — pas parfois, toujours, selon le rythme auquel quelqu'un oublie.
La projection qui décrit est celle que l'on interroge, donc : openapi.Live(app) appelle app.Fiber().GetRoutes(true), et toute autre fonction de openapi/ est une fonction pure du []Route qui revient. Le document n'est pas construit à partir du routeur. Le document est le routeur, sous une autre forme.
Pourquoi lire le routeur vivant est la seule source totale
L'alternative évidente est de balayer le source. Cela ne marche pas, et la raison mérite d'être dite avec précision, parce que c'est elle qui fait qu'une grande partie de l'outillage d'API sous-déclare, en silence.
POST /v1/kms/auth/login est enregistré ainsi :
Group("/v1/kms/auth").Post("/login")
Aucun grep ne trouve ce chemin. La chaîne /v1/kms/auth/login n'existe pas dans le code. Elle n'existe que dans le routeur assemblé, à l'exécution, une fois que le framework a concaténé le préfixe du groupe et la feuille.
Ensuite cela empire, d'une façon qui est en réalité correcte. L'ensemble des routes est une fonction de la configuration de déploiement — cfg.Enabled, plus des portes internes comme le if kc != nil de KMS. Un déploiement qui ne monte pas admin ne sert pas les routes admin. La spécification honnête de ce déploiement ne les annonce donc pas.
C'est pourquoi le document est généré par processus, au moment de la requête, plutôt que construit une fois dans la CI. Une spécification construite dans la CI décrit un déploiement maximal hypothétique, qui n'est peut-être pas celui auquel vous parlez. Une spécification lue sur le routeur vivant décrit le processus qui vous a répondu.
Le test qui le rend vrai
Une architecture où la spécification ne peut pas dériver est une affirmation, et les affirmations ont besoin d'une porte. La nôtre est une bijection, dans cmd/cloud/openapi_test.go :
func TestSpecDoesNotDriftFromLiveRouter(t *testing.T) {
Il monte chaque sous-système dans apps.Wire(), génère le document et affirme la correspondance dans les deux sens : chaque route vivante apparaît comme une opération, et chaque opération est portée par une route vivante. Pas un sous-ensemble dans un sens ni dans l'autre.
Lancé aujourd'hui :
bijection holds over 1362 operations / 963 paths / 138 products
C'est le seul test du dépôt dont l'échec signifie que le document publié ment. Tout le reste qui casse rend le produit faux ; celui-ci rend le contrat faux, ce qui est pire, parce que les intégrateurs ont déjà construit contre lui.
Que le compte bouge n'est pas un problème — il bouge chaque fois qu'un sous-système est ajouté. Le nombre qui compte est la différence entre les deux côtés, et ce nombre est zéro ou la compilation échoue.
L'axe produit est mécanique
Le premier segment de chemin après /v1/ est le produit. openapi.Product le lit et en étiquette chaque opération, ce qui permet à une CLI de construire hanzo <product> <resource> <verb> sans que personne exerce de jugement sur l'endroit où une commande doit vivre.
Ce n'est délibérément pas le nom du sous-système. clients/billing sert aussi /v1/finance/*, et l'URL est ce que voit l'appelant, donc l'URL l'emporte. Nommer l'axe d'après le paquet interne aurait fait de la CLI un artefact tenu à la main — une chose qui contient des opinions, qu'il faut maintenir — au lieu d'une projection.
Ce que le routeur ne peut pas vous dire
Voici la limite, et nous n'avons pas l'intention de la contourner.
La méthode, le chemin, les paramètres de chemin et le produit se déduisent du routeur. Les schémas de requête et de réponse, les paramètres de query et d'en-tête, les codes de statut et l'authentification, non. Le routeur détient un func(*zip.Ctx) error. Le type de la requête est une variable locale dans le corps du handler :
var req secretPutRequest
json.Unmarshal(ctx.Body(), &req)
Go ne peut pas réfléchir d'une valeur de fonction vers son corps. Il n'y a pas d'astuce ici, et en tenter une dans le générateur voudrait dire deviner — c'est ainsi qu'une spécification acquiert des entrées assurées et fausses.
Le seul chemin vers les schémas est une opération typée :
zip.Get[In, Out](...)
In et Out sont sur l'enregistrement, donc le schéma est disponible sans lire dans les pensées de quiconque. Et, parce qu'ils sont sur l'enregistrement, la même entrée produit aussi un outil MCP — zip/openapi.go et zip/mcp.go sont deux lecteurs d'un seul registre. C'est la quatrième projection, et elle est arrivée sans quatrième mécanisme.
La migration est incrémentale par construction : GetRoutes() est un sur-ensemble de app.ops, donc convertir un handler en opération typée ajoute le schéma sans rien changer en amont.
Les routes qui ne peuvent pas être typées, et pourquoi
clients/integrations est converti aussi loin que le fil le permet : 20 routes sur 41 sont des opérations typées. Les 21 autres restent des handlers bruts, et chacune est listée dans une carte rawRoutes de ops_projection_test.go avec la raison pour laquelle elle ne peut pas bouger :
- 13 redirections de navigateur. Le callback OAuth et chaque maillon du lien répondent
302. Une opération typée renvoie une valeur, pas un en-têteLocationet un statut dans les 300. - 6 webhooks. HMAC de Slack et de GitHub, Ed25519 de Discord, JWT du Bot Framework de Teams, l'en-tête secret-token de Telegram. Chacun authentifie une signature sur les octets bruts de la requête. Une opération typée reçoit un
Indéjà décodé et ne voit jamais les octets sur lesquels la signature a été calculée, donc les typer casserait leur authentification. - 2 créateurs de
202 Accepted.cloud.Creatednous donne une jointure 201. Il n'y a pas de jointure 202. Deux routes en ont besoin, donc deux routes attendent.
Nous pensons que cette liste est l'artefact le plus précieux de tout le système. C'est un inventaire énuméré, épinglé par des tests, de l'endroit exact où l'abstraction s'arrête, avec une raison par entrée qu'un relecteur peut contester. Le mode d'échec qu'elle empêche est celui où une route redevient en silence un handler brut et disparaît d'un coup du document, de l'ensemble d'outils MCP et de la CLI — de façon invisible, parce que rien n'affirmait qu'elle s'y trouvait.
Une architecture n'est pas les parties qui se généralisent. Ce sont les parties qui se généralisent, plus un compte rendu honnête et exigé des parties qui ne le font pas.
Ce que cela achète
Quelqu'un qui intègre Hanzo Cloud lit un document produit par le processus qui répond à ses requêtes, dans le déploiement auquel il parle vraiment, il y a quelques millisecondes. Un agent qui appelle via MCP atteint le même handler, avec le même contrôle de tenant, qu'un appelant REST — un appel anonyme à /mcp reçoit le même 403 que REST, parce que c'est le même chemin de code et qu'un test le fixe.
Rien de tout cela n'a exigé que quelqu'un pense à régénérer quoi que ce soit.
La forme générale : si deux artefacts doivent s'accorder, ne générez pas l'un à partir de l'autre en espérant. Faites de l'un une vue de l'autre, et écrivez le test qui échoue quand ils ne le sont pas.
Lire la suite

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

Rendre le commerce headless lisible à l'ère des LLM agentiques
Les acheteurs deviennent des agents. Ce qu'il faut pour être lisible, signable et payable par un LLM sans humain dans la boucle, et ce que Hanzo livre aujourd'hui.

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.