API dizaynı: sistemlər arasında müqavilə
API dizaynı: sistemlər arasında müqavilə
Ch04-də dedik ki, interface-lər vədlərdir. Module-lar bir process daxilində danışanda vədi pozmaq hələ ucuzdur: compiler xəbər verir, bütün kod bir commit-də düzəldilir. Şəbəkə üzərindən danışan sistemlərdə isə vəd başqa komandaların, mobil tətbiqlərin və heç vaxt görmədiyiniz partnyorların kodunda yaşayır. Onu geri götürmək aylar çəkir.
Bu fəsil həmin vədi şəbəkə səviyyəsində konkretləşdirir: hansı stil nəyə yarayır, böyük API-lərdə hansı detallar həlledicidir və API-ni consumer-ləri sındırmadan necə inkişaf etdirmək olar.
Contract-first: API-ni consumer-in gözü ilə dizayn etmək
Ən çox yayılmış yanaşma API-ni sonda, implementation-dan "çıxarmaqdır": cədvəllər var, ORM model-ləri var, onları JSON-a çevirib endpoint açırıq. Nəticə ch04-dəki sızan lüğətdir. Daxili schema xarici contract-a çevrilir və hər refactoring breaking change olur.
Contract-first yanaşmada ardıcıllıq tərsinədir. Əvvəlcə contract yazılır (OpenAPI, Protobuf və ya GraphQL schema), consumer-lərlə müzakirə olunur, sonra implementation qurulur. Bunun iki faydası var. Birincisi, sual "bizdə hansı data var?" yox, "consumer nə etmək istəyir?" olur. İkincisi, contract kod yazılmazdan əvvəl review edilə bilən artefakt olur və mock server-lər sayəsində client komandası paralel işləyə bilir.
Qiyməti əlavə addımdır. Bir komandanın öz frontend-i üçün yazdığı, birlikdə deploy olunan daxili endpoint üçün bu formallıq çox vaxt artıqdır. Amma API komanda sərhədini keçirsə, contract-first adətən özünü doğruldur.
REST: resource-lar və HTTP semantikası
REST sistemi əməliyyatlar yox, resource-lar toplusu kimi modelləşdirir: /orders, /orders/42, /orders/42/items. Nə etmək istədiyinizi isə HTTP metodu deyir. Gücü ondadır ki, bu semantika bütün internet infrastrukturu tərəfindən başa düşülür: proxy-lər, cache-lər, browser-lər və monitoring alətləri.
- GET oxuyur, heç nəyi dəyişmir. Ona görə cache oluna və təhlükəsiz təkrarlana bilər.
- PUT resource-u tam əvəz edir, DELETE silir. Hər ikisi idempotent-dir: ikinci çağırış nəticəni dəyişmir.
- POST yeni resource yaradır və ya əməliyyat icra edir. İdempotent deyil: iki dəfə göndərilən POST iki sifariş yarada bilər.
- PATCH qismən dəyişiklikdir və idempotent olub-olmaması formatdan asılıdır.
Status kodları da contract-ın hissəsidir. 4xx "sən səhv etdin, təkrarlama", 5xx "mən səhv etdim, bəlkə sonra alınar" deməkdir. Validation xətası üçün 500 qaytaran API client-ləri mənasız retry-lara məcbur edir. Hər xəta üçün 200 qaytarıb body-də "success": false yazan API isə monitoring-i kor edir. Ch07-dəki SLI-lar status kodlarına baxırsa, bu yalan birbaşa SLO-ya sızır.
REST-in zəif tərəfi hər şeyin resource-a sığmamasıdır. "Sifarişi ləğv et" əməliyyatını DELETE /orders/42 kimi ifadə etmək yanlışdır, çünki sifariş silinmir. Praktikada POST /orders/42/cancel kimi əməliyyat endpoint-ləri normaldır. Puristlik naminə domain-i əymək lazım deyil.
RPC və gRPC: əməliyyat yönümlü dizayn
RPC resource-lar yox, function-lar dilində danışır: CreateOrder, CancelOrder, GetShippingQuote. Uzaq çağırış lokal function çağırışı kimi görünür. Bu həm rahatlıq, həm də tələdir: şəbəkə çağırışı lokal çağırışdan min dəfə yavaşdır və uğursuz ola bilər. Interface bunu gizlədirsə, ch04-dəki chatty interface-lər asanlıqla yaranır.
gRPC bu ideyanın müasir, geniş yayılmış formasıdır. Contract Protobuf-da yazılır, hər dil üçün client və server kodu generasiya olunur. Binary serialization və HTTP/2 sayəsində mesajlar JSON-dan kiçikdir və parse etmək daha ucuzdur. Bundan əlavə, client, server və bidirectional streaming birinci sinif imkanlardır. Protobuf-un field nömrələri isə additive təkamülü təbii edir.
Qiyməti: browser gRPC-ni birbaşa danışa bilmir, gRPC-Web və ya proxy lazımdır. Binary trafiki curl ilə oxumaq olmur, debugging üçün xüsusi alətlər gərəkir. HTTP caching isə praktiki olaraq yoxdur. Ona görə gRPC-nin təbii yeri daxili service-lər arasıdır: latency və throughput vacibdir, hər iki tərəf sizin nəzarətinizdədir və kod generasiyası tip təhlükəsizliyi verir.
GraphQL: client-driven query-lər
GraphQL-də server bir schema açır, client isə tam olaraq hansı field-lərə ehtiyacı olduğunu bir query-də yazır. Bu, REST-in iki klassik problemini həll edir: over-fetching (100 field gəlir, 3-ü lazımdır) və under-fetching (bir ekran üçün beş ardıcıl request). Fərqli ekranları olan mobil və web client-lər üçün bu, böyük qazancdır. Frontend komandası backend-dən yeni endpoint gözləmədən irəliləyə bilir.
Amma çeviklik mürəkkəbliyi server tərəfə köçürür:
- N+1 problemi. 50 sifariş və hər birinin müştərisi üçün naiv resolver-lər 51 database sorğusu edir. DataLoader kimi batching mexanizmləri məcburidir, opsional deyil.
- HTTP caching. Bütün query-lər adətən bir
POST /graphqlendpoint-inə gedir, ona görə CDN və browser cache-i işləmir. Persisted query-lər bunu qismən həll edir. - Query mürəkkəbliyi. Client istənilən dərinlikdə query yaza bilər. Depth və complexity limitləri olmadan bir query bütün database-i yükləyə bilər.
Üç stili yan-yana qoyaq:
| REST | gRPC | GraphQL | |
|---|---|---|---|
| Coupling | Orta: resource modeli, HTTP semantikası | Yüksək: ortaq .proto, generated kod | Schema-ya bağlı, amma client field seçir |
| Performance | Yaxşı, JSON overhead-i ilə | Ən yüksək: binary, HTTP/2, streaming | Dəyişkən: resolver-lərdən və query-dən asılı |
| Tooling | Ən geniş: curl, OpenAPI, hər dil | Güclü code generation, zəif ad-hoc debugging | Güclü introspection, schema explorer-lər |
| Caching | HTTP caching təbii işləyir | Praktiki olaraq yoxdur | Çətin, application səviyyəsində |
| Browser dəstəyi | Tam | Proxy və ya gRPC-Web lazımdır | Tam |
| Ən uyğun yer | Public API-lər, partnyor inteqrasiyaları | Daxili service-lər arası, aşağı latency | Müxtəlif client-li frontend-lər, BFF qatı |
Çox vaxt cavab "hansı biri?" yox, "harada hansı?" olur: kənarda REST və ya GraphQL, içəridə gRPC.
Böyük API-lərin praktik məsələləri
Pagination. Offset pagination (?offset=10000&limit=50) sadədir, amma database hər dəfə 10 000 sətri keçib atmalıdır. Səhifələr arasında yeni yazı əlavə olunanda isə element təkrarlanır və ya itir. Cursor pagination (?after=eyJpZCI6...) "son gördüyüm elementdən sonrakıları ver" deyir. Index üzərində sabit sürətlə işləyir və dəyişən data-da ardıcıl qalır. Qiyməti ondadır ki, "47-ci səhifəyə keç" mümkün olmur. Böyük və tez dəyişən data üçün bu, adətən məqbul güzəştdir.
Filtering və sorting. Hər field üzrə filter açmaq cazibədardır, amma hər filter gələcəkdə dəstəklənməli olan vəd və potensial index-siz sorğudur. Real ehtiyaclarla başlayın.
Idempotency key-lər. POST-u retry etmək təhlükəlidir, amma şəbəkədə retry qaçılmazdır:
Client
POST /paymentsgöndərir, server ödənişi keçirir, amma cavab timeout-a düşür. Client bilmir ki, ödəniş keçib, yoxsa yox. Retry etsə, müştəridən iki dəfə pul çıxıla bilər. Həll: client hər əməliyyat üçün unikalIdempotency-Key: 7f3a...header-i göndərir. Server key-i nəticə ilə birlikdə saxlayır və eyni key ilə gələn təkrar request-ə yeni ödəniş yaratmadan əvvəlki cavabı qaytarır.
Rate limit-lər. Limiti tətbiq etmək kifayət deyil, onu görünən etmək lazımdır. Hər cavabda qalan kvotanı göstərən header-lər, 429-da isə Retry-After yaxşı davranan client-lərə özlərini tənzimləmək imkanı verir.
Versioning və təkamül
Ən yaxşı versioning strategiyası versiyaya ehtiyac yaratmamaqdır. Additive dəyişikliklər (yeni optional field, yeni endpoint, yeni enum dəyəri, əgər client-lər tolerant reader-dirsə) heç kimi sındırmır. Breaking change-lər (field silmək, tipi və ya mənanı dəyişmək) isə yeni versiya tələb edir.
Versiyanı harada daşımaq barədə iki əsas seçim var. URL-də (/v2/orders) görünən, sadə və cache-friendly-dir, amma bütöv API-ni birdən versiyalamağa təşviq edir. Header-də (Accept: application/vnd.shop.v2+json) URL-lər təmiz qalır, amma debugging və test çətinləşir. Hər ikisi işləyir. Vacib olan ardıcıllıq və deprecation siyasətidir: köhnə versiya nə qədər yaşayacaq, consumer-lərə necə xəbər veriləcək (Deprecation və Sunset header-ləri, birbaşa əlaqə), kimin hələ də onu çağırdığını necə izləyəcəksiniz.
Və hər zaman yadda saxlayın Hyrum's law-u: kifayət qədər consumer varsa, sistemin müşahidə olunan hər davranışından kimsə asılı olacaq. Sənədləşdirilməmiş sıralama, xəta mesajının mətni, cavab vaxtı. Contract sizin yazdığınız deyil, consumer-lərin faktiki istifadə etdiyidir. Bu səbəbdən contract test-ləri sənədlərdən daha etibarlı qoruyucudur.
Köhnə versiyalar həm də security riskidir: deprecate olunmuş, amma hələ də cavab verən endpoint-lər çox vaxt yeni versiyanın control-larından məhrum qalır. Bu və API-lərin digər security məsələləri CyberSecurity kitabının 7-ci fəslində əhatə olunub.
Synchronous və asynchronous API-lər
Hər əməliyyat bir HTTP request-in ömrünə sığmır. Video emalı, böyük hesabat və ya bank köçürməsi dəqiqələr çəkə bilər. Bağlantını bu qədər açıq saxlamaq timeout-lara və boşa gedən resurslara səbəb olur.
Standart pattern: server request-i qəbul edir, 202 Accepted və status resource-u (/jobs/91) qaytarır. Client sonra nəticəni iki yolla öyrənə bilər:
- Polling. Client vaxtaşırı status-u soruşur. Sadədir, firewall problemi yoxdur, amma lazımsız trafik yaradır və gecikmə polling intervalı qədərdir.
- Webhook-lar. Server iş bitəndə client-in URL-inə özü request göndərir. Effektivdir, amma indi siz client-siniz və onun bütün problemlərini daşıyırsınız: alıcı əlçatmaz ola bilər (backoff ilə retry lazımdır), eyni webhook iki dəfə çata bilər (alıcı idempotent olmalıdır) və hər kəs o URL-ə saxta request göndərə bilər (payload signature ilə imzalanmalıdır).
Yaxşı praktika ikisini birləşdirməkdir: webhook sürət üçün, status endpoint-i isə itən webhook-lar üçün ehtiyat yol kimi. Bir neçə consumer-in reaksiya verməli olduğu daxili hadisələr üçün isə ch03-dəki event-driven yanaşma, yəni broker üzərindən event-lər, adətən daha uyğundur.
Hazırkı sisteminiz haqqında suallar
- Ən çox istifadə olunan API-nizin response-u daxili database model-ini nə qədər əks etdirir? Növbəti schema refactoring-i hansı consumer-ləri sındırardı?
- Pul və ya stok kimi vacib state dəyişdirən POST endpoint-ləriniz client retry-ına davam gətirirmi? Timeout-dan sonra təkrar request nə edir?
- Deprecated etdiyiniz son endpoint-i kimlərin hələ də çağırdığını bilirsinizmi? Bunu bu gün ölçə bilərsinizmi?
- Hansı uzun əməliyyatınız hələ də synchronous request daxilində icra olunur və timeout-a ən yaxın olan hansıdır?