Tahle kapitola není o tom, co tvůj backend dělá, ale jak je poskládaný uvnitř — kam který kus kódu patří. Většina backendů totiž neumře na špatný algoritmus, ale na chaos: pravidla appky rozházená všude možně, jedna malá změna rozbije pět nesouvisejících věcí, a po půl roce se v tom nikdo nevyzná. Dobrá architektura tomu předchází. Vezmeme to od základních pojmů.
Pár pojmů na úvod
- Byznys logika = pravidla tvojí aplikace, ta „chytrá" část. Např. „uživatel nesmí mít víc než 3 aktivní objednávky" nebo „slevu lze uplatnit jen jednou". To je srdce appky.
- Controller (route handler) = kód, který přijme příchozí HTTP request a rozhodne, co s ním.
- Service = kód, kde žije byznys logika.
- Repository = kód, který se stará o ukládání a čtení z databáze.
- ORM = knihovna, přes kterou pracuješ s databází pomocí objektů místo psaní SQL.
- Závislost (dependency) = něco, co jeden kus kódu potřebuje k práci (např. service potřebuje repository, aby se dostal k datům).
Nelekej se, projdeme to teď na jednoduchém příměru.
Vrstvy — jako restaurace
Nejjednodušší způsob, jak backend uspořádat, je rozdělit ho do vrstev, kde každá má jednu práci. Představ si restauraci:
- Controller = číšník. Převezme od hosta požadavek, zkontroluje, že dává smysl, předá ho do kuchyně a pak donese výsledek. Sám nevaří — žádná byznys logika, žádné SQL.
- Service = kuchař. Tady se odehrává ta „chytrá" práce podle pravidel (ověř, že má uživatel místo na další objednávku, spočítej cenu se slevou). Neřeší, jak přišel požadavek (HTTP?), ani kde jsou data uložená.
- Repository = spíž. Umí jen donést a uložit data („dej mi uživatele s ID 5"). Service neřeší, jestli je za tím Postgres, nebo něco jiného.
Pravidlo, které si zapamatuj: tencí číšníci, vytížení kuchaři, hloupá spíž (thin controllers, fat services, dumb repositories). Když začneš vařit u stolu (byznys logika v kontroleru), nejde to pak znovupoužít odjinud (třeba ze zpracování fronty) ani pořádně otestovat.
Separation of Concerns — každý dělá jen svoje
Hlavní myšlenka výše má jméno: separation of concerns (oddělení starostí). Každý kus kódu má řešit jednu věc. Když jedna funkce zároveň parsuje HTTP, počítá cenu, posílá e-mail a píše do databáze, nejde ji bezpečně změnit — sáhneš na jedno a rozbiješ druhé.
Dva pojmy, které k tomu patří:
- Cohesion (soudržnost) — věci, co spolu souvisí, jsou pohromadě. Chceš ji vysokou.
- Coupling (provázanost) — jak moc jeden modul závisí na druhém. Chceš ho nízký.
Cíl: vysoká soudržnost, nízká provázanost. Každý modul dělá jednu věc pořádně a o ostatních ví co nejmíň.
Hexagonal architecture — když to chceš zatáhnout dál
Jakmile appka roste, používá se pokročilejší uspořádání (uvidíš pojmy hexagonal architecture, ports & adapters, clean architecture — je to v zásadě totéž). Myšlenka: byznys logika (doména) je uprostřed a neví vůbec nic o vnějším světě. Všechno vnější — HTTP, databáze, fronta, e-mail — jsou adaptéry, které se připojují přes porty (rozhraní = domluvený „zásuvkový" tvar).
Klíčové pravidlo (dependency rule): závislosti směřují dovnitř, k doméně. Doména nezávisí na databázi; je to naopak — databáze se přizpůsobí doméně. Výhoda: vyměníš databázi za jinou, nebo HTTP za zpracování fronty, a jádro appky se nezmění. A jádro se dá otestovat úplně bez databáze.
Ale pozor na opačný extrém. Pro malou appku jsou plné vrstvy a porty zbytečná ceremonie. Začni jednoduše (klidně controller → service → ORM) a přidávej strukturu, až když složitost reálně roste. Architektura ti má sloužit, ne být náboženství.
Kam patří byznys logika
Nejčastější chyba začátečníka: napsat pravidla appky do kontroleru nebo přímo do databázových modelů. Patří do service / doménového modelu. Dva přístupy, jak to uspořádat:
- Anemic model — entity jsou jen „pytlíky na data" bez chování a všechna logika je v service. Jednoduché a běžné, ale logika se může rozlézt do moc míst.
- Rich domain model — entita si sama hlídá svoje pravidla (
objednavka.zrus()ověří, že se zrušit dá). Logika je u dat, hůř se rozteče. Je to srdce DDD (viz níže).
Ať zvolíš cokoli: byznys logika NIKDY nepatří do kontroleru ani do databázové vrstvy.
DDD-lite — pár užitečných pojmů
DDD (Domain-Driven Design) je celá filozofie návrhu kolem domény. Nemusíš ji dělat celou, ale tyhle pojmy se ti budou hodit:
- Entity — věc s identitou, která přetrvává změny (uživatel #42 je pořád ten samý, i když změní jméno).
- Value object — věc definovaná svou hodnotou, neměnná (peníze, adresa).
Money(100, 'CZK'); dvě stejné částky jsou zaměnitelné, nemají vlastní identitu. - Aggregate — skupina entit, které se mění jako celek přes jeden „hlavní" objekt (objednávku
a její položky měníš jen přes
Order). Drží data konzistentní. - Ubiquitous language — kód mluví jazykem byznysu: metoda se jmenuje
rezervujMisto(), neupdateRow(); třída jeFaktura, neDataRow2. Ať kód čte i člověk z byznysu.
DTO a mapování mezi vrstvami
Data nemají mít všude stejný tvar. Jinak vypadá entita v databázi (viz výše) a jinak to, co pošleš klientovi přes API. DTO (Data Transfer Object) je prostý objekt bez logiky, jehož jediný úkol je přenést data přes hranici (typicky mezi API a klientem) ve správném tvaru.
Proč nikdy neposílat klientovi přímo databázovou entitu:
- Únik citlivých polí — entita
Usermá ipasswordHash,isAdmin. Když ji pošleš celou, vyzradíš je (bezpečnostní díra, viz Security). - Provázanost — API navázané na databázi se rozbije klientům při každé změně schématu (přejmenuješ sloupec → spadne mobilní appka).
- Jiný tvar — klient často chce data jinak, než leží v databázi.
Proto se na hranici vytvoří DTO: request DTO (co přijímáš — hned ho zvaliduješ a vezmeš jen
povolená pole, aby klient nepodstrčil isAdmin: true) a response DTO (co posíláš — jen pole, co
má vidět). Při průchodu aplikací tak data mění tvar a mapper je mezi tvary převádí:
Pozor i tady na opačný extrém: u triviální appky můžou být tři skoro stejné tvary zbytečná byrokracie. Začni jednoduše a odděluj DTO, až ti chybějící hranice začne dělat problémy (únik polí, rozbíjení API).
Dependency Injection — předávej závislosti zvenčí
Když si nějaká třída sama vyrobí to, co potřebuje (new PostgresRepo()), je s tím napevno
svázaná. Dependency Injection (DI) znamená, že jí to potřebné předáš zvenčí (typicky v
konstruktoru):
// ❌ napevno svázané – v testu to nejde obejít, potřebuješ reálnou databázi
class UserService { repo = new PostgresRepo() }
// ✅ injektované – v testu předáš falešné (fake) repo, žádná databáze
class UserService { constructor(private repo: UserRepository) {} }
Výhody: snáz se testuje (podstrčíš falešnou závislost), snadno vyměníš implementaci a je jasně vidět, na čem třída závisí. Právě DI dělá hexagonální architekturu prakticky použitelnou.
Struktura složek: podle vrstev, nebo podle feature?
| Podle vrstev (by layer) | Podle feature (doporučeno, jak appka roste) | |
|---|---|---|
| Složky | controllers/, services/, repos/ | users/, orders/, billing/ |
| Výhoda | Jasně vidět vrstvy | Vše k jedné funkci pohromadě, snadno smazat/přesunout |
| Nevýhoda | Jedna funkce roztažená po 5 složkách | Vrstvy si musíš hlídat uvnitř každé feature |
Malý projekt: „podle vrstev" stačí. Jak roste, „podle feature" se obvykle udržuje líp (souvisí s modulárním monolitem v Tech Choices).
Anti-patterny (čeho se vyvarovat)
- 🚩 Byznys logika v kontroleru / v databázovém modelu — nejde testovat ani znovupoužít.
- 🚩 God object — jedna obří třída „UserManager" o 2000 řádcích, co dělá úplně všechno.
- 🚩 Circular dependencies — A potřebuje B, B potřebuje A. Známka špatně vedených hranic.
- 🚩 Premature architecture — pět vrstev a tucet rozhraní na appku se třemi endpointy.
Failure modes — jak to dopadne, když architekturu zanedbáš
- Spaghetti kód → každá změna má nečekané vedlejší efekty, protože všechno souvisí se vším.
- Big ball of mud → žádné jasné hranice; po roce nikdo neví, kde co je.
- Over-engineering → tolik abstrakcí, že přidat jeden endpoint trvá celý den.
- Netestovatelnost → logika přilepená na HTTP a databázi → testy potřebují celý systém.
🛠️ Cvičení
- Rozvrstvi chaos. Vezmi (klidně smyšlený) endpoint, který v jednom kuse kódu validuje vstup, počítá cenu, ukládá do databáze a posílá e-mail. Rozděl ho na controller → service → repository.
- Injektuj závislost. Přepiš
class OrderService { repo = new PostgresRepo() }tak, aby repo dostával v konstruktoru. Pak naskicuj test, kde mu předáš falešné repo bez databáze. - Navrhni DTO. Entita
Usermáid,email,passwordHash,isAdmin,createdAt. Navrhni response DTO pro veřejný profil — co v něm bude a co ne a proč. A co zvaliduješ u request DTO? - Kam patří pravidlo? Pro „uživatel nesmí mít víc než 3 aktivní objednávky" urči, do které vrstvy logika patří a proč ne do kontroleru.
- Anemic vs rich. Naskicuj objekt
Orderdvakrát: jednou anemicky (data + service zvlášť), jednou richly (order.zrus()si hlídá pravidla sám). Pojmenuj výhody a nevýhody. - Podle vrstev vs podle feature. Načrtni stejnou appku (users, orders, billing) v obou strukturách složek a řekni, kdy která začne být nepříjemná.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
- Rozvrstvi chaos — controller jen přijme request, zvaliduje vstup a zavolá service; service spočítá cenu a řídí pravidla; repository ukládá; e-mail je další závislost service (přes port). Pointa: „tencí číšníci, vytížení kuchaři" — past je nechat výpočet ceny nebo SQL v kontroleru, pak to nejde znovupoužít z fronty ani otestovat.
- Injektuj závislost — z
repo = new PostgresRepo()udělášconstructor(private repo: UserRepository); v testu předáš fake repo, které vrací data z paměti, žádná databáze. Na co pozor: typuj parametr na rozhraní (UserRepository), ne na konkrétníPostgresRepo— jinak ti DI nepomůže a fake nepodstrčíš. - Navrhni DTO — response DTO pro veřejný profil:
id,email(pokud má být vidět),createdAt; nikdypasswordHashaniisAdmin(únik). U request DTO vezmeš jen povolená pole a zvaliduješ je. Pointa: nikdy nevracej entitu celou ani nezapisuj naslepo, čím klient pošle — jinak si podstrčíisAdmin: true. - Kam patří pravidlo? — „max 3 aktivní objednávky" je byznys logika → patří do service / doménového modelu, ne do kontroleru. Důvod: v kontroleru jde svázané s HTTP, nejde znovupoužít z fronty/cronu a špatně se testuje (test by potřeboval celý HTTP stack).
- Anemic vs rich — anemic:
Orderje pytlík dat, pravidla vOrderService(jednoduché, ale logika se rozleze); rich:order.zrus()si pravidla hlídá sám (logika u dat, hůř se rozteče). Pointa: obojí je legitimní — ať zvolíš cokoli, logika nikdy nesmí skončit v kontroleru ani v databázové vrstvě. - Podle vrstev vs podle feature — „podle vrstev" (
controllers/,services/) je jasné u malé appky, ale jedna funkce se roztáhne po 5 složkách; „podle feature" (users/,orders/) drží vše pohromadě a líp škáluje. Na co pozor: u feature struktury si vrstvy musíš hlídat uvnitř každé feature, jinak se chaos jen přesune dovnitř.
🧠 Otázky & odpovědi
Proč byznys logika nepatří do kontroleru?
Kontroler je svázaný s HTTP. Logika v něm nejde znovupoužít z jiného vstupu (zpracování fronty, cron, příkazová řádka), špatně se testuje (test potřebuje celý HTTP stack) a má sklon nabobtnat do obří nepřehledné třídy. Patří do service / doménového modelu, který o HTTP ani o konkrétní databázi nic neví. Kontroler má jen přijmout, zvalidovat, zavolat service a vrátit odpověď.
Co říká dependency rule v hexagonální architektuře?
Závislosti směřují dovnitř, k doméně. Byznys logika nezávisí na ničem vnějším; naopak okolní technologie (databáze, HTTP, fronta) se přizpůsobují doméně tím, že implementují její rozhraní (porty). Důsledek: vyměníš databázi nebo HTTP za zpracování fronty a jádro appky se nezmění — a otestuješ ho bez databáze.
Jak dependency injection zlepšuje testovatelnost?
Když si třída sama vyrábí závislosti (new PostgresRepo()), je s nimi pevně srostlá a v testu je nejde
obejít → test potřebuje reálnou databázi. S dependency injection dostane závislost zvenčí, takže
v testu jí podstrčíš falešnou (fake) implementaci. Testy jsou tím rychlejší, spolehlivější a je jasně
vidět, na čem třída závisí.
Jaký je rozdíl mezi entity, value object a aggregate?
Entity má identitu, která přetrvává změny (uživatel #42 i po přejmenování). Value object je
definovaný svou hodnotou a je neměnný (peníze, adresa — dvě stejné jsou zaměnitelné, nemají vlastní
identitu). Aggregate je skupina entit, které se mění jako celek přes jeden hlavní objekt
(objednávku a její položky měníš jen přes Order), čímž drží data konzistentní.
Co je DTO a proč neposílat klientovi přímo databázovou entitu?
DTO (Data Transfer Object) je prostý objekt bez logiky, jehož úkol je přenést data přes hranici (mezi
API a klientem) ve správném tvaru. Databázovou entitu neposílej přímo ze tří důvodů: únik citlivých
polí (passwordHash, isAdmin), provázanost (změna databáze pak rozbije klienty) a jiný
tvar (klient chce data jinak, než leží v databázi). Proto na hranici sestavíš DTO jen s tím, co má
klient vidět (response DTO), a u příchozích dat (request DTO) vezmeš jen povolená pole a zvaliduješ je.
Kdy je architektura over-engineering?
Když přidává abstrakce, které neřeší žádný současný problém. Pět vrstev a tucet rozhraní na jednoduchou appku se třemi endpointy = ceremonie, co jen zpomaluje. YAGNI (You Aren't Gonna Need It): začni jednoduše (klidně controller → service → ORM) a přidávej strukturu, teprve když složitost reálně roste. Architektura má sloužit, ne být sama sobě cílem.
