API Design

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

Co se naučíš: Budeš umět navrhnout API, které se dá verzovat, bezpečně opakovat a jehož chyby se dají zpracovat strojově.

13 min čtení + cvičeníNavazuje na:🌐 Foundations

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 API, GET /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. A pozor i na souběžný retry (klient pošle podruhé, než první doběhne): naivní „zkontroluj, pak založ" má sám race condition, proto dej na klíč UNIQUE a zakládej ho atomickým insertem, ať druhý request narazí (celý vzor v Patterny).
  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.