Un contract, mai mulți clienți

ForkTex Engineering · 5 septembrie 2026 · 3 min

Un backend și clienții lui cad de acord asupra unui contract. Întrebarea e doar dacă acel acord e scris undeva pe care o mașină îl poate verifica, sau dacă trăiește în capul oamenilor care s-au întâmplat să fie în cameră.

Noi îl scriem. Schema OpenAPI a API-ului e contractul unic, iar fiecare artefact care vorbește cu el, stratul de date TypeScript, SDK-ul Python, suprafața de unelte pentru agenți, e generat din acea schemă, nu scris manual împotriva ei.

Șiretlicul care trece de review

Eșecul pe care îl previne asta e subtil, de-asta e atât de comun.

Cineva are nevoie de un singur endpoint de la un serviciu. A genera un client întreg pare greoi, așa că scrie cincisprezece linii:

async function fetchInvoice(id: string): Promise<Invoice> {
  const res = await fetch(`/api/invoices/${id}`);
  return res.json() as Promise<Invoice>;
}

Asta trece de review. E clar, e mic și funcționează. Va continua să funcționeze exact până când cineva redenumește un câmp pe server.

În acel moment nimic nu se rupe zgomotos. Interfața Invoice încă se compilează. e o afirmație scrisă de mână despre o formă, nu o verificare față de una. Fetch-ul încă returnează 200. Câmpul e pur și simplu undefined, iar eșecul iese la suprafață trei straturi mai departe, într-o componentă care randează gol unde ar trebui un total, sau mai rău, într-un raport care e tacit greșit.

Șiretlicul n-are nicio legătură cu ruta pe care o oglindește. Nimic nu le conectează pe cele două, așa că nimic nu-ți poate spune că au divergat.

Generează, apoi compară

Soluția e să generezi clientul din schemă. Asta e jumătatea bine-cunoscută, și singură nu e suficientă. Pentru că un fișier generat care e comis poate fi editat manual, iar o editare manuală supraviețuiește dacă nimic n-o contrazice.

Deci generatorul primește colți. CI regenerează fiecare artefact și compară cu ce e comis. Orice diferență pică build-ul.

Această singură verificare transformă generatorul din consultativ în autoritar. Fără ea, secvența e previzibilă: cineva ajustează un fișier generat ca să rezolve ceva urgent, ajustarea funcționează, nimeni nu regenerează o lună, iar până atunci fișierul comis și schema nu sunt de acord, fără nicio evidență a cui are dreptate. Cu diferența verificată, acea editare pică imediat, cât timp motivul ei e încă proaspăt.

Aceeași verificare se aplică oricărui alt lucru derivat dintr-o sursă de adevăr. Un fișier generat spune asta la început, numește comanda care îl regenerează, și e comparat în CI. Fără comparație, primele două sunt doar decor.

Două straturi, unul dintre ele niciodată atins

Codul generat rareori e toată povestea. Clienții au nevoie de autentificare, retry-uri, un URL de bază, gestionare de ciclu de viață. Lucruri pe care nicio schemă nu le descrie.

Deci fiecare client e două straturi: o jumătate generată pe care nimeni n-o editează, și un strat subțire scris manual deasupra ei. Interceptorii de autentificare, reîmprospătarea token-ului și wrapper-ele de confort trăiesc în stratul scris manual. Tipurile de request și response trăiesc în cel generat.

Granița e ce face aranjamentul stabil. Când schema se schimbă, regenerarea înlocuiește complet stratul inferior, iar stratul superior rămâne neatins. Când autentificarea se schimbă, stratul superior se schimbă și niciun tip nu se mișcă. Niciuna dintre schimbări n-o amenință pe cealaltă, ceea ce e exact ce eșuează când cele două sunt împletite într-un singur fișier întreținut manual.

Rețeaua are o formă, și nu e a lui Python

Încă un acord care merită făcut explicit, pentru că eșuează tacit într-un mod deosebit de enervant.

Formatul nostru de rețea e camelCase. Python-ul nostru e snake_case. În loc să traducem câmp cu câmp sau endpoint cu endpoint, fiecare model de request și response moștenește o bază comună care aplică automat conversia și acceptă oricare formă la intrare. TypeScript trimite startDate, Python citește start_date, și nu există niciun strat de traducere care să iasă din sincronizare.

Ratezi baza comună, moștenești modelul simplu în schimb, și nimic nu semnalează eroare. Serverul serializează fericit start_date pe o rețea unde fiecare client așteaptă startDate. Clientul citește undefined dintr-un câmp prezent sub alt nume, iar bug-ul pare o valoare lipsă, nu o nepotrivire de denumire.

Asta e prinsă de verificarea de divergență, nu de un test, ceea ce e o împărțire rezonabilă a muncii: testele dovedesc comportament, comparația dovedește derivarea.

Framework-uriBiblioteciBaze de DateBiblioteci ForkTex
Adopția în sistemele ForkTex, calculată din registrul de tehnologii.

Ce câștigi

O schimbare de schemă devine mecanică. Schimbi endpoint-ul, regenerezi, iar verificatorul de tipuri enumeră fiecare loc de apel care are nevoie de atenție. Înainte ca ceva să fie lansat, într-o singură trecere, fără ca cineva să-și amintească ce clienți există.

Alternativa nu e că munca asta dispare. E că se întâmplă mai târziu, un incident pe rând, în orice consumator la care nimeni nu s-a gândit.