API Design

REST, idempotency keys, verzování, pagination, error contracts.

API je rozhraní, přes které tvůj backend nabízí svoje funkce okolnímu světu — frontendu, mobilní aplikaci, jiné službě. Je to smlouva: „když mi pošleš tenhle požadavek, udělám tohle a vrátím ti tamto." A jakmile tu smlouvu někdo začne používat, nemůžeš ji jen tak změnit, aniž bys mu rozbil aplikaci. Dobrý návrh API je hlavně o ohleduplnosti k tomu, kdo ho bude volat.


Pár pojmů na úvod

  • API (Application Programming Interface) = sada „vstupních bodů" tvého backendu, které může někdo zavolat. Ne pro člověka jako web, ale pro jiný program.
  • Endpoint = jeden konkrétní vstupní bod = kombinace cesty a metody, např. GET /courses nebo POST /orders.
  • Zdroj (resource) = věc, se kterou API pracuje (kurz, objednávka, uživatel).
  • Payload / tělo (body) = data, která v requestu posíláš nebo v response dostáváš (obvykle JSON).
  • Klient (konzument API) = ten, kdo tvoje API volá. Může to být tvůj vlastní frontend i cizí appka.

Vycházíme z Foundations — HTTP metody a status kódy už znáš, teď je použijeme k návrhu rozumného API.


Styly API — tři přístupy

Existují tři hlavní způsoby, jak API postavit. Nejsou to soupeři, každý se hodí jinam:

StylJak se na to dívatKdy ho zvolit
RESTPracuješ se zdroji přes HTTP metodyDefault. Veřejná API, běžné CRUD
gRPCVoláš funkci na vzdálené služběKomunikace mezi tvými vlastními službami, kde jde o rychlost
GraphQLKlient si řekne přesně o data, která chceKdyž klient (často mobil) potřebuje pokaždé něco jiného

Jako junior začni s REST — je nejrozšířenější a všechno ostatní pochopíš snáz, až ho budeš mít v ruce. gRPC a GraphQL si necháš, až narazíš na konkrétní potřebu.


REST pořádně

Hlavní myšlenka REST: zdroj je podstatné jméno, akci určuje HTTP metoda. Do URL tedy nepatří slovesa — co s daným zdrojem děláš, říká už metoda (GET, POST…).

✅ GET    /courses             vrať seznam kurzů
✅ GET    /courses/42          vrať detail kurzu 42
✅ POST   /courses             vytvoř kurz
✅ PUT    /courses/42          nahraď celý kurz 42
✅ PATCH  /courses/42          uprav část kurzu 42
✅ DELETE /courses/42          smaž kurz 42
✅ GET    /courses/42/lessons  lekce patřící ke kurzu 42

❌ GET  /getCourses            (sloveso v URL — to dělá GET)
❌ POST /createCourse          (sloveso v URL — to dělá POST)
❌ POST /courses/42/delete     (na mazání je DELETE)

Připomenutí sémantiky metod (z Foundations): GET jen čte (nic nemění, jde cachovat), PUT a DELETE jsou idempotentní (zopakování nic nezkazí), POST idempotentní není — a právě proto existují idempotency keys ↓.


Idempotency keys — jak bezpečně opakovat POST

Připomeň si pojem idempotentní z Foundations: operace, kterou můžeš provést vícekrát se stejným výsledkem. Problém je, že POST (vytvoř něco) idempotentní není.

Představ situaci: klient pošle POST /payments, ale spojení vyprší (timeout) dřív, než dorazí odpověď. Klient neví, jestli platba proběhla. Co udělá? Zkusí to znovu (retry) — a když ta první ve skutečnosti prošla, právě jsi zákazníkovi naúčtoval dvakrát.

Řešení: klient ke každému takovému requestu přiloží unikátní klíč (Idempotency-Key). Server si podle něj pozná, že už ten požadavek viděl:

Server si páruje klíč → výsledek (třeba v Redisu nebo databázi, na omezenou dobu). Podrobně, včetně záludností, v kapitole Patterny → Idempotency. Tohle je naprostý základ, který by měl umět každý backend engineer.


Verzování — jak měnit API a nerozbít staré klienty

Tvoje API se bude vyvíjet, ale aplikace, které ho už používají, nesmí ze dne na den přestat fungovat.

  • Přes URL: /v1/courses, /v2/courses — nejjasnější a nejčastější způsob.
  • Přes hlavičku: verzi pošleš v hlavičce requestu — čistší URL, ale hůř vidět.
  • Pravidlo: přidat nové (volitelné) pole je neškodné. Odebrat nebo přejmenovat pole, nebo změnit jeho typ, je breaking change (rozbije klienty) → dělej to v nové verzi.

⚠️ Nová major verze není zadarmo — znamená udržovat dvě implementace souběžně. Proto se s ní šetří: většina změn jde udělat zpětně kompatibilně (aditivně), bez nové verze. A když starou verzi rušíš, udělej to řízeně: ohlas to dopředu (hlavičky Deprecation/Sunset) a dej klientům čas přejít. Neverzuj při každé změně.

Pagination — nikdy nevracej „všechno"

Kdyby GET /orders vrátilo všech 5 milionů objednávek, zabiješ tím databázi i klienta. Proto se výsledky vrací po stránkách (pagination). Dva způsoby:

TypJak se voláVýhody / nevýhody
Offset?limit=20&offset=40 (přeskoč 40, dej 20)Jednoduchý, umí skočit na stránku N / pomalý na velkých datech a nestabilní (když mezitím někdo přidá řádek, stránky se posunou)
Cursor?limit=20&cursor=abc (dej 20 za bodem abc)Stabilní a rychlý i na velkých datech / neumí skočit rovnou na stránku N

Pro nekonečný scroll a velké tabulky používej cursor. Offset si nech na malé, neměnné seznamy.


Error contract — jednotný tvar chyb

Klient musí umět chybu zpracovat strojově, ne jen přečíst. Proto vracej chyby pořád ve stejném tvaru napříč celým API:

{
  "success": false,
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Vstup neprošel validací",
    "details": [{ "field": "email", "issue": "invalid_format" }]
  }
}
  • code je stabilní řetězec pro stroj (kód na něj může reagovat), message je text pro člověka. Klientovi nikdy neposílej syrový technický výpis chyby (stack trace) — je to únik informací a bezpečnostní riziko.
  • Vracej k tomu správný HTTP status (4xx = chyba klienta, 5xx = chyba serveru).
  • Pole details patří k validačním chybám (422 — které konkrétní pole je špatně). Business konflikt (409, např. „kurz je plný") nemá konkrétní vadné pole — vrať jen code a message (třeba code: "COURSE_FULL"), žádné details necpi.

Další věci, co dělají API dobrým

  • Konzistentní pojmenování — vyber si snake_case nebo camelCase a drž ho všude stejně.
  • Filtrování a řazení přes query parametry: ?status=active&sort=-createdAt.
  • Rate limiting — omez počet requestů na klienta, ať ti někdo API nezahltí; přes kód 429 (Security).
  • Dokumentace — popiš API standardem OpenAPI/Swagger, ideálně generovaným z kódu, ať nezestárne.
  • Bezpečnost — každou operaci, co mění data, ověř (kdo a jestli smí), vstup validuj na serveru a nikdy nevěř tomu, co přijde od klienta (viz Security).

Bulk operace, dlouhé operace a API gateway

Tři situace, na které REST „základ" nestačí a je dobré vědět, jak na ně:

  • Bulk operace (hromadné). Potřebuješ vytvořit/upravit 100 položek? Posílat 100 requestů je pomalé. Nabídni endpoint, který vezme pole položek najednou. Pak ale řeš částečný úspěch — co když projde 98 a 2 selžou? Vrať výsledek pro každou položku zvlášť, ne jen „OK / chyba".
  • Dlouhé operace (async API). Když operace trvá dlouho (vygenerovat velký report), nenech klienta čekat na jednu odpověď. Vrať hned 202 Accepted s odkazem „stav úlohy je tady", úlohu zpracuj na pozadí a klient si stav buď polluje (občas se zeptá), nebo mu po dokončení pošleš webhook.
  • API gateway. Když máš víc služeb, postavíš před ně jednu bránu, která řeší společné věci na jednom místě: ověření, rate limiting, směrování requestů na správnou službu. Klient mluví s bránou, ne přímo se službami. (Varianta BFF = Backend For Frontend: brána ušitá na míru konkrétnímu klientovi, třeba mobilu.)

Failure modes — kde se to nejčastěji pokazí

  • Chybějící idempotency → duplicitní platby/objednávky, když klient po timeoutu zopakuje request.
  • Breaking change bez nové verze → rozbiješ produkční klienty bez varování.
  • Offset pagination na milionech řádků → pomalé a přeskakující/duplikující záznamy.
  • Únik interních detailů v chybách → posíláš klientovi stack trace nebo SQL = bezpečnostní díra.

🛠️ Cvičení

  1. Navrhni REST API. Pro „kurzy a jejich lekce" napiš endpointy: výpis a detail kurzu, vytvoření a úprava kurzu, výpis lekcí kurzu a zápis studenta. Bez sloves v URL, se správnými metodami a status kódy.
  2. Idempotentní POST. Naskicuj v pseudokódu handler POST /payments s Idempotency-Key: kde klíč ukládáš, jak poznáš opakovaný request a jak vrátíš uloženou odpověď.
  3. Cursor vs offset. Pro nekonečný scroll feedu napiš, jaké query parametry použiješ, a vysvětli, proč offset na milionu řádků začne přeskakovat nebo duplikovat záznamy.
  4. Error contract. Navrhni jednotný tvar chyby a přiřaď k němu 4 situace i s HTTP kódy: neprošla validace, nepřihlášen, nemá právo, konflikt (např. plný kurz).
  5. Breaking change? U každé změny rozhodni, jestli je breaking (a tedy potřebuje novou verzi): přidání volitelného pole, přejmenování pole, zúžení typu, nový povinný parametr, změna formátu data.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
  1. Navrhni REST APIGET /courses, GET /courses/42, POST /courses (201), PUT/PATCH /courses/42, GET /courses/42/lessons, zápis studenta jako POST /courses/42/enrollments. Pointa: žádná slovesa v URL (zdroj = podstatné jméno, akci určuje metoda); past je dělat POST /courses/42/enroll místo zápisu nového zdroje.
  2. Idempotentní POST — uložíš dvojici klíč → výsledek (Redis/DB), na začátku ověříš, jestli klíč už existuje: pokud ano, vrátíš uloženou odpověď a neprovedeš znovu. Na co pozor: klíč musíš uložit atomicky se samotnou operací, jinak vznikne okno, kdy platba projde, ale klíč ne → duplicita.
  3. Cursor vs offset — pro feed ?limit=20&cursor=abc (ukazatel na poslední položku). Pointa: offset na milionu řádků přeskakuje/duplikuje, protože když mezi requesty někdo přidá řádek, celé okno se posune; cursor se váže na konkrétní bod, takže je stabilní (jen neumí skočit na stránku N).
  4. Error contract — jednotný tvar { success, error: { code, message, details? } }: neprošlá validace 422 (+ details po polích), nepřihlášen 401, nemá právo 403, plný kurz 409 (jen code, např. COURSE_FULL, bez details). Pozor: details patří jen k validaci; do business konfliktu je necpi a klientovi nikdy neposílej stack trace.
  5. Breaking change? — breaking jsou: přejmenování pole, zúžení typu, nový povinný parametr a změna formátu data (rozbijí klienty → nová verze); přidání volitelného pole je bezpečné. Pointa: aditivní změny dělej zpětně kompatibilně, novou major verzi si nech jen na ty breaking.

🧠 Otázky & odpovědi

Proč POST není idempotentní a jak to vyřešíš pro platby?

Zopakování POSTu vytvoří nový záznam nebo akci — retry po timeoutu tak může znamenat dvě platby. Řešení je idempotency key: klient pošle s requestem unikátní klíč a server si páruje klíč → výsledek. Když dorazí stejný klíč podruhé, operaci neprovede znovu, jen vrátí uloženou odpověď. Klíč se musí uložit atomicky se samotnou operací, jinak vznikne okno, kdy operace proběhne, ale klíč ne → duplicita.

Kdy použít cursor a kdy offset pagination?

Offset (limit/offset) je fajn na malé, neměnné seznamy a když chceš umět skočit na konkrétní stránku — ale na velkých datech je pomalý a nestabilní (když mezi requesty někdo přidá řádek, okno se posune a dostaneš duplikáty nebo přeskočíš). Cursor (ukazatel na poslední položku) je stabilní a rychlý i na velkých a měnících se datech a hodí se na nekonečný scroll; neumí jen skočit na stránku N.

Co je breaking change a jak se mu bránit?

Změna, která rozbije existující klienty: odebrání nebo přejmenování pole, zúžení typu, nový povinný parametr, změna významu. Přidat volitelné pole je naopak bezpečné. Breaking změnu zaveď v nové verzi (/v2) a starou nech běžet, dokud klienti nepřejdou. Zásada: buď tolerantní k tomu, co přijímáš, a opatrný v tom, co měníš ve výstupu.

Jak má vypadat dobrá chyba z API?

Vždy stejný tvar napříč API: strojově čitelný stabilní code (např. INSUFFICIENT_STOCK), lidsky čitelný message a volitelně details pro chyby po jednotlivých polích. K tomu správný HTTP status (4xx chyba klienta, 5xx chyba serveru). Klientovi nikdy neposílej stack trace ani SQL — je to únik informací, který pomáhá útočníkovi.

Proč začít s REST a kdy teprve gRPC nebo GraphQL?

REST je univerzální, snadno se cachuje a rozumí mu každý → ideální default i pro učení. gRPC (rychlý, typovaný) se hodí na komunikaci mezi tvými vlastními službami, kde jde o nízkou latenci. GraphQL zvol, když klient (typicky mobil) potřebuje pokaždé jinou sadu dat a chceš se vyhnout posílání zbytečností — za cenu složitějšího serveru a obtížnějšího cachování.

Jak navrhnout API pro operaci, která trvá dlouho?

Nenech klienta čekat na jednu pomalou odpověď (překročí timeout). Vrať hned 202 Accepted s odkazem na „stav úlohy", samotnou práci zpracuj na pozadí, a klient si stav buď polluje (občas se zeptá „už hotovo?"), nebo mu po dokončení pošleš webhook. Tím oddělíš „přijal jsem úkol" od „úkol je hotový".

K čemu je API gateway?

Je to jedna brána před tvými službami, která na jednom místě řeší společné věci: ověření, rate limiting a směrování requestu na správnou službu. Klient mluví s bránou, ne přímo se službami. Varianta BFF (Backend For Frontend) je brána ušitá na míru konkrétnímu klientovi (např. mobilu), aby dostával přesně ta data v takovém tvaru, jaký potřebuje.