Teorija: jedna obična aplikacija, od prototipa do stotina zahtjeva u sekundi, blok po blok. Praksa: blok jedan, najobičniji backend za vaš projekat, sa agentom.
Skoro dvije godine u Coinis-u. Počeo kao full stack developer, brzo prešao u mediora, a od nedavno Agentic Product Lead, Growth Products.
Neko je sjedio i pisao boilerplate: DTO, mapiranja, konfiguraciju, isti CRUD po deseti put.
Neko je sjedio i pisao mail template. Za svaki servis. Ručno.
Neko je dobio najnebitniji read endpoint na najobičnijoj aplikaciji i to mu je bio dan. Ništa teško, ništa zanimljivo, i to je bio posao.
O tome više ne razmišljamo. Boilerplate, template, endpoint: agent, sat vremena, pročitate diff.
MVP i prototip su lagani. Za par dana imate zategnut prototip koji izgleda savršeno i radi za vas, za deset ljudi.
Pitanje koje ostaje: hoće li to izdržati kad se otvori?
Ko za sebe kaže backend, full stack, software engineer: ne vrednuje ga se više po tome koliko brzo kuca, nego po tome šta odluči da se napravi i da li to podnese opterećenje.
Isti endpointi napisani za prototip, ista baza. Realni opsezi, u zahtjevima u sekundi ka backendu:
Interni alat, paywall, složena registracija. Ovo preživi bilo kakva arhitektura; jedan server i SQLite su dovoljni.
Jedan dobar server i jedna baza rade, pod uslovom da su upiti u redu. Jedan spor upit i sve stane.
Google, SEO crawleri, scraperi, konkurencija. Ovo ne podnese bilo kakva arhitektura: tu počinju blokovi o kojima danas pričamo.
Estitor, najveći portal za nekretnine u Crnoj Gori, običan radni dan: ~820 zahtjeva u sekundi ka backendu, veliki dio su crawleri.
Zato se ovo predavanje zove „Backend i AI”, a zapravo je o arhitekturi: odluke koje agent ne donosi umjesto vas. Ono što se danas zove software architect.
Martin Fowler: arhitektura su odluke koje je skupo mijenjati. Koja baza. Koliko servera. Idu li fajlovi kroz server. Korisnik nijednu ne vidi, a svaka određuje koliko košta sljedeća funkcionalnost.
Loša arhitektura radi. Pa se svaka nova stvar radi sporije i skuplje, dok jednog dana ne stane.
Ko je imao situaciju: „radi na mom laptopu", a kod nekog drugog ne? Danas učimo zašto, i šta se radi da se ne desi.
Nijedan blok koji danas nacrtamo nije tu „jer se tako radi". Svaki je odgovor na pitanje. Idemo graditi.
Obična aplikacija, kakvu i vi pravite: oglasi sa slikama, prijava, lista i detalj. Prvi korisnici dolaze. Jedan server. Idemo odatle, blok po blok.
Ovo nije loše. Za prvih sto korisnika, za MVP, za vaš projekat danas u praksi: tačno ovo treba. Ne pravite mikroservise za aplikaciju koju koristi vaša tetka.
Nestane struje. Disk otkaže (nije „ako", nego „kad"). Kolega uradi rm -rf u pogrešnom folderu. Šta ostaje?
Stanje je sve što ne možete regenerisati iz koda: podaci korisnika, fajlovi, istorija. Bilo je vezano za mašinu. Ko izgubi mašinu, izgubi biznis.
Baza ide na svoju mašinu. Svako radi jedan posao: server služi zahtjeve, baza čuva stanje. Kad nešto pukne, znate gdje gledate.
I fajlovi odlaze sa servera, na zaseban disk. Server više ne pamti ništa. Umre? Podignete drugi iz gita, pokažete mu gdje su baza i disk, radi.
Najčešće se krši nenamjerno: sesija „privremeno" u memoriji, upload na lokalni disk. Radi mjesecima, dok ne dodate drugi server.
Prvi instinkt: kopiraj mašinu. Server A i server B, svaki sa svojom bazom. Gdje je problem?
Dva servera, dvije istine. Korisnik sačuva oglas na A, sljedeći zahtjev ode na B koji ne zna ništa. Ne pada, ne baca error, samo povremeno laže. Rješenje: jedna baza. Server smije umrijeti, baza ne smije.
Deset hiljada ljudi, server na 100% CPU. Dodali smo drugi. Ali korisnik ukuca oglasi.me: koja IP adresa? Ko odlučuje kome ide koji zahtjev?
Round robin: redom, 1, 2, 3, 1… Najčešće dovoljno. Least connections: kome je najlakše, taj dobija; upload slika i GET liste nisu isti posao.
Health check: svake sekunde GET /health. Kliknite na server da ga „ubijete": ispada iz rotacije, korisnici ne primijete. Napišite /health u svakom servisu koji ikad napravite.
Load balancer pušta da dodajemo servere. Ali kapacitet se dodaje na dva načina. Koji biste vi?
Više jezgara i RAM-a, kod se ne mijenja. Ali ima plafon, najveća mašina koju neko prodaje, a jedan pad je i dalje sve palo.
Bez plafona. Autoscaling ih dodaje i gasi po opterećenju: min 2 (jedan smije umrijeti), max 10; u 20 h osam, u 4 ujutru dva. Plaćate ono što koristite.
Horizontalno radi samo zato što su serveri stateless: load balancer smije da pošalje korisnika bilo kome. „Sticky session" (isti korisnik uvijek na isti server) zvuči zgodno, a najčešće je zakrpa za stanje koje se negdje sakrilo.
Korisnik objavi oglas sa osam slika po 10 MB. Sto korisnika u istom minutu. Naivno: POST /upload, server primi fajl u memoriju, upiše u bazu kao BLOB. Šta se dešava ostalih deset hiljada?
Blok 3: metapodaci prije bajtova. Najgore: bajtovi kroz server u bazu. Bolje, ali i dalje loše: kroz server pa u storage (S3, R2, svejedno), server i dalje nosi svaki bajt dvaput. Optimalno: API upiše red (slika, oglas, status pending), vrati privremeni link, korisnik šalje direktno u bucket. Server ne vidi nijedan bajt. Ovo pravilo ide u vaš CLAUDE.md.
Treba thumbnail i smanjena verzija za listu, notifikacija pratiocima, indeks za pretragu. Storage pozove servis za obradu slika, a on je baš tada pao.
Događaj umjesto poziva. Storage javi jednom: „slika 123 stigla". Broker (Kafka, RabbitMQ, SQS) poruku trajno zapiše i dostavi svima koji slušaju, svako svojim tempom. Obrada pala? Poruka čeka.
Poruka je zapisana na disk, ne samo u memoriji. Broker se restartuje, poruke su tu.
Nema ack za X sekundi? Stiže opet. Posljedica: isti posao može doći dvaput, pa mora biti idempotentan. Napraviti isti thumbnail dvaput: isti fajl. Naplatiti karticu dvaput: katastrofa.
Poruka koja pada deset puta ne blokira red zauvijek. Ide u DLQ, stiže alarm na Slack. Znate ŠTA je propalo, ne otkrivate za tri dana.
Jedan događaj, pet primalaca. Sjutra dodate šesti servis: samo se pretplati, storage ne dirate.
Pauza 15 minuta. Poslije pauze problem prestaje da bude tehnički i postaje ljudski: tim od 30 ljudi.
Backend je jedan kod u tri kopije, pretraga izdvojena jer ima drugačiji load. Svako od njih ima javnu adresu i sam provjerava token, broji zahtjeve, završava TLS.
Blok 5. API gateway je jedini javni ulaz: provjerava token, broji zahtjeve, rutira po putanji (nginx, Traefik, Kong; u cloudu AWS API Gateway, Cloudflare). Sve iza njega je privatna mreža bez javnih adresa. Ovo nije poziv na mikroservise: iza gateway-a je i dalje monolit, plus ono što ste stvarno izdvojili.
Drugačiji load ili resurs (pretraga sa svojim indeksom, obrada slika koja troši CPU). Drugačiji ritam promjene ili drugi jezik. Posao koji smije da padne, a da aplikacija nastavi da radi.
U našoj aplikaciji to su pretraga i obrada slika. Oglasi, korisnici i naplata ostaju zajedno: dijele iste tabele i iste transakcije.
Poziv između servisa je mrežni poziv: kasni i otkazuje. Ono što je bilo jedan COMMIT sad ide preko dva servisa. Svaki ima svoj deploy, logove, monitoring; za debug treba tracing.
Primjer: „kupi premium paket" je u monolitu jedan COMMIT nad dvije tabele. Sa posebnim servisom za naplatu postaje: naplati, pozovi oglase, ako padne vrati novac. Taj kod neko mora da napiše i testira.
Podrazumijevano: monolit sa jasnim modulima, što je app/<domen>/ u vašem backendu. Servis se izdvaja kad za to imate izmjeren razlog, ne unaprijed.
KO SI. Radi gateway, jednom, na ulazu. Potpis i rok tokena.
ŠTA SMIJEŠ. Radi svaki servis sam: smiješ li da mijenjaš baš ovaj oglas. U praksi: 403 kad nisi vlasnik.
Korisnici, Google, scraperi. Svaki zahtjev: gateway → oglasi → baza. Isti upit „daj oglase, Podgorica, najnoviji", osamsto puta u sekundi. Zašto to radimo?
Cache-aside: prvo pitaj RAM; nema → čitaj bazu, upiši u keš sa TTL-om, vrati. U keš idu podaci, nikad bajtovi slika. Druga greška: invalidacija. Cijena se promijeni u bazi, keš još 30 s laže. Ili je to prihvatljivo, ili na svaku izmjenu brišete ključ.
Lista po gradu i datumu bez indeksa čita cijelu tabelu: 400 ms. Sa indeksom na (city, created_at): 2 ms. Prvo pitanje kad je upit spor.
8 servera × 10 konekcija = 80 na bazu, gornja granica ma koliko zahtjeva stiglo. Bez pool-a svaki zahtjev otvara svoju; Postgres klekne oko 500. PgBouncer ispred baze.
Upisi na primar, čitanja na kopije koje kasne milisekunde. Lista oglasa smije da kasni 50 ms; stanje računa ne smije, ono ide sa primara.
Redoslijed: indeks, pa keš, pa pool, pa tek replika. Svaki korak je deset puta jeftiniji od sljedećeg, a agent prvi predloži posljednji.
Isti princip kao Redis, keš ispred sporog izvora, samo za fajlove i geografski. Prvi korisnik iz Podgorice napuni edge PoP slikom oglasa, ostalih 49.999 dobija sa 5 ms. Isto važi za JS i CSS vašeg frontenda: statika ide sa edge-a, ne sa vašeg servera.
Agresivan crawler, scraper konkurencije, ili vaš sopstveni frontend poslije greške: N+1 poziv u listi, retry bez pauze. Autoscaling to posluša i dodaje servere; račun na kraju mjeseca raste.
Na gateway-u, prije servisa. Brojač po IP-u ili korisniku u Redisu, jer ga svi gateway serveri dijele.
429 Too Many Requests i header Retry-After: 60. Servis iza gateway-a zahtjev nikad ne vidi.
Najčešći „napadač" je sopstveni kod: N+1 poziv sa frontenda, retry bez backoff-a, cron koji se preklopio. Limit ga zaustavi na 11. zahtjevu, prije računa.
Svaka strelica je jedna odluka koju smo danas donijeli zajedno, i jedno mjesto gdje može da pukne. Kad sljedeći put vidite ovakav dijagram, ne pitajte „šta je ovo", nego „koji problem je ovo riješilo". Ako ne nađete problem, blok je višak.
Redoslijed nije slučajan: svaki problem nastaje tek kad prethodni riješite. Gradite samo ono što vam trenutno treba, i znate šta je sljedeće.
Vaš projekat, za sat vremena, je blok 1 sa JWT-om iz bloka 5. Ostalo znate prepoznati kad zatreba. Pitanja o crtežu, prije nego pređemo na baze i alate?
Aplikacija je prošla put. Usput je dobila Postgres, Redis, object storage i broker: četiri mjesta za podatke, i to nije kraj. Ko je to odlučio, i po čemu?
Tabele sa vezama, ključevi, JOIN, transakcije: „naplati pa aktiviraj", oboje ili ništa. SQL koji razumije svaki developer i svaki agent. Postgres uz to ima JSONB kolone, full-text i pgvector.
Sve što ima strukturu i mora biti tačno: korisnici, oglasi, paketi, naplata. Podrazumijevani izbor; devedeset posto aplikacija nikad ne treba drugu bazu.
Jedan primar za upise; vertikalno skaliranje ima plafon, šardovanje je ručni posao. Milion upisa u sekundi nije za njega. Košta malo: open source, jedna mašina ide jako daleko.
Instagram je prve godine, sa 30 miliona korisnika i tri inženjera, vodio na Postgresu i on je i danas jezgro. Reddit, Apple (interno), Stripe (Postgres i MongoDB).
Kad ne znate šta da uzmete, uzmite ovo. Drugu bazu dodajte tek kad izmjerite problem koji ova ne rješava.
Ključ → vrijednost, sve u RAM-u, odgovor u mikrosekundama. Strukture: brojači, liste, skupovi, TTL na svakom ključu. Nema upita „daj mi sve gdje je…".
Keš (cache-aside iz bloka 6), brojači za rate limit, sesije i refresh tokeni, redovi i pub/sub za sitne poslove. Sve što smije da nestane jer se napuni iz baze.
RAM je najskuplji gigabajt koji kupujete, deset puta skuplji od SSD-a; podaci stanu koliko stane memorije. Trajnost je opciona, pa nije izvor istine. Jedan proces: velike vrijednosti blokiraju sve.
Twitter je timeline svakog korisnika držao u Redisu; Stack Overflow kešira stranice u Redisu ispred SQL Servera; GitHub, Snapchat. AWS ga prodaje kao ElastiCache, Cloudflare i Amazon forkovali u Valkey.
Prva baza koju dodate poslije Postgresa, gotovo uvijek. Pravilo: u Redis ide ono što smijete izgubiti.
JSON dokument po ključu, bez fiksne šeme; svaki zapis može imati drugačija polja. Čitanje cijelog dokumenta jednim potezom. DynamoDB i Firestore su potpuno upravljani, naplata po zahtjevu.
Kad zapisi nisu isti: stan ima sprat i grijanje, auto kilometražu, i ne želite 40 nullable kolona. Korpa, profil, konfiguracija. DynamoDB: ista milisekunda na milion zahtjeva u sekundi.
JOIN-ove radite vi, u kodu; šemu morate modelovati po tome kako čitate, ne po tome šta imate. Transakcije preko više dokumenata su ograničene. DynamoDB po zahtjevu ume da bude skup pri pogrešnom modelu.
Amazonova korpa je rođena u Dynamu (2007), zato da radi i kad pola data centra padne. Lyft i Disney+ na DynamoDB-u. eBay, Forbes i EA na MongoDB-u. Postgres JSONB pokriva većinu ovih slučajeva bez nove baze.
Prije nego uzmete Mongo, probajte JSONB kolonu u Postgresu. Dokument baza dobija smisao kad je šema zaista različita ili kad vam treba upravljana baza bez servera.
Tabele bez JOIN-a, raspoređene po particijskom ključu na desetine mašina, bez jednog primara: svaki čvor prima upise. Pad mašine ili cijelog regiona ne zaustavlja upis.
Ogroman broj upisa koji nikad ne stane i čitanje po ključu: poruke po razgovoru, istorija gledanja po korisniku, metrike sa uređaja. Milion upisa u sekundi je normalan radni dan.
Upit mora da prati particijski ključ; „daj mi sve oglase jeftinije od X" ne postoji. Nema transakcija preko particija. Skupo u ljudima: klaster od desetina čvorova traži nekoga ko ga razumije.
Netflix drži istoriju gledanja svakog korisnika u Cassandri (milijarde upisa dnevno, mora preživjeti pad AWS regiona). Discord je milijarde poruka držao u Cassandri, pa prešao na ScyllaDB. Apple vodi jedan od najvećih Cassandra klastera na svijetu.
Tek kad Postgres fizički ne može da primi upise. Za aplikaciju sa oglasima: nikad u prvoj godini.
Podaci se čuvaju po kolonama, pa „prosjek cijene po gradu" čita samo dvije kolone od pedeset, komprimovane deset puta. Napravljene za agregacije preko milijardi redova, ne za izmjenu jednog reda.
ClickHouse: događaji uživo, upit u milisekundama, jeftin po upitu, vaš server. Snowflake i BigQuery: izvještaji, terabajti, naplata po računanju, nula administracije; uz njih sirovi podaci u S3/Parquet za cente po GB.
Zašto klikove ne pišemo pravo u Snowflake? Može, i radi se (Snowpipe Streaming), ali upit traje sekunde i plaća se svaki put: za „koliko klikova ima ovaj oglas", koje se traži na svakom prikazu, treba ClickHouse ili Redis brojač. Nijedna nije za transakcije ni za UPDATE jednog reda.
Cloudflare analitiku svakog HTTP zahtjeva (milioni u sekundi) servira iz ClickHouse-a; Uber i eBay logove isto. Spotify izvještaje vrti na BigQuery-ju, Instacart i Canva na Snowflake-u.
Dva pitanja odlučuju: ko gleda rezultat (korisnik na stranici ili analitičar jednom dnevno) i koliko puta (hiljadu puta u sekundi ili deset puta na dan). Vruće u ClickHouse, hladno u Snowflake + S3.
Invertovani indeks: za svaku riječ lista dokumenata u kojima se javlja. Tolerancija na greške u kucanju, sinonimi, ranking po relevantnosti, filteri i faceti u istom upitu.
Pretraga kakvu korisnici očekuju: „dvosoban podgorca" nađe dvosobne stanove u Podgorici. Logovi i njihovo pretraživanje (ELK). Meilisearch za male timove: jedan binarni fajl, radi za sat.
Nije izvor istine: indeks je kopija Postgresa koja se puni preko brokera i smije da kasni sekundu. Gladan RAM-a; klaster je skup. Postgres full-text pokriva jednostavnu pretragu bez nove baze.
Wikipedia pretraga radi na Elasticsearch-u. Uber, Netflix (logovi), GitHub (code search godinama), Booking. Algolia i Meilisearch kao upravljane alternative za e-commerce i portale.
Dodajte je kad korisnici traže riječima, a ne filterima; dotle Postgres full-text. I nikad kao jedino mjesto gdje podatak živi.
Svaki tekst ili slika postane vektor, niz od recimo 1.536 brojeva (embedding) koji opisuje značenje; „slično" je udaljenost dva vektora. Baza indeksira vektore tako da najbližih deset nađe za milisekunde.
„Slični oglasi", „pronađi stranice slične ovoj", semantička pretraga, i RAG: AI asistent koji prvo nađe vaše dokumente, pa ih da modelu. Radionica 6 je ovdje.
Embedding se plaća po tokenu i mora se ponovo računati kad promijenite model. Kvalitet zavisi od modela više nego od baze. Pinecone je upravljan i naplaćuje po vektoru; pgvector je besplatan ali na velikim skupovima sporiji.
Notion AI pretražuje milijarde blokova preko Pinecone-a. Supabase gura pgvector kao podrazumijevano. Spotify i Instacart imaju sopstvene vektorske sisteme za preporuke.
Počnite sa pgvector-om u Postgresu koji već imate; posebna vektorska baza tek kad izmjerite da je spor.
Čvorovi i veze su prvoklasni podaci; upit prati veze: „prijatelji mojih prijatelja koji su gledali ovaj oglas". Ono što je u SQL-u pet JOIN-ova, ovdje je jedan korak dublje.
Kad su veze važnije od zapisa: preporuke po mreži, detekcija prevare (dva naloga, isti telefon, isti IP, ista kartica), prava pristupa, kuda-koji-dio-ide u logistici.
Skupo skaliranje preko više mašina, mali ekosistem i malo ljudi koji ga znaju. Za jednostavne veze Postgres sa dvije tabele radi isti posao. Rijetko prva baza, često treća.
LinkedIn je sagradio sopstveni graf za „ljudi koje možda poznajete". NASA Neo4j-em pretražuje decenije inženjerskih izvještaja. Banke i platne kuće ga koriste za prevare: PayPal, Zurich. Airbnb ga je probao za knowledge graph.
Uzmite ga kad vam upit u SQL-u ima pet JOIN-ova i još je spor. Dotle: tabela veza u Postgresu.
| Šta čuvamo | Kako se koristi | Tehnologija |
|---|---|---|
| Oglasi, korisnici, premium paketi, naplata | relacije, transakcije („naplati pa aktiviraj"), tačnost | PostgreSQL |
| Keš liste, brojači, rate limit, sesije | mikrosekunde, RAM, smije da nestane | Redis |
| Polja različita po kategoriji (stan: sprat, auto: km) | JSON po ključu, šema se mijenja | Postgres JSONB · MongoDB |
| Poruke između korisnika, milijarde, po razgovoru | upisi koji ne staju, čitanje po ključu, bez JOIN-a | Cassandra / ScyllaDB |
| Prikazi i klikovi: uživo na stranici / izvještaji | milion događaja/s; ms upiti za korisnika / terabajti jednom dnevno | ClickHouse Snowflake + S3 |
| Pretraga „dvosoban sa pogledom na more" | full-text, greške u kucanju, ranking; kopija, ne istina | Elasticsearch |
| „Slični oglasi", asistent nad oglasima | sličnost vektora (embedding) | pgvector / Pinecone |
| Prevare: dva naloga, isti telefon, ista kartica | veze važnije od zapisa | Neo4j |
Polyglot persistence: više baza, svaka za svoj oblik podataka. Crveni red je ono što ubija Postgres: milion upisa u sekundi. Sve ispod prve dvije linije dolazi tek kad izmjerite problem.
Tabele sa vezama, dokumenti, ključ-vrijednost, vektori?
I koliko brzo mora: milisekunde za korisnika, minuti za izvještaj?
Keš smije, naplata ne smije.
Hot u RAM-u, warm na SSD-u, cold u S3 za cente.
Pravilo: počnite sa PostgreSQL-om. Za 90% aplikacija je dovoljan (ima JSON kolone, full-text i pgvector). Drugu bazu dodajte tek kad izmjerite problem koji prva ne rješava, i onda onu koja rješava baš taj problem.
Kliknite framework za detalje, ili filter iznad da vidite ko je dobar za šta.
Jezik AI ekosistema: svaki LLM SDK, svaka ML biblioteka prvo izađe za Python. Radionica 6 i vaši budući projekti sa agentima su tu.
Pydantic šeme su Zod za Python: tipovi i validacija u jednom. Automatski /docs (OpenAPI) koji čitaju i frontend i agent. Starter je složen kao moji produkcijski servisi: domeni, servisi, repozitorijumi, DI.
SQLite je fajl: nula instalacije na 35 laptopova. Isti kod, jedan DATABASE_URL, i radi na Postgresu u Dockeru.
| Radionica 3 (frontend) | Danas (backend) |
|---|---|
features/<name>/types.ts | models/ (tabela) + schemas/ (JSON ulaz/izlaz) |
| Zod šema u formi | Pydantic Field(min_length=3, max_length=80), ista pravila, 422 sa fieldErrors |
api/<name>-api.ts (mock) | api/ + services/ + repositories/; frontend fajl dobija fetch umjesto niza |
docs/entities.md | Ostaje ugovor. Mijenja se u istom commitu kao kod. |
| TanStack Query hook-ovi | Ne mijenjaju se. Zato je pravilo „podaci samo kroz api/" bilo važno. |
Pored frontend repoa od prošle subote nastaje backend repo: baza, CRUD API, prijava sa tokenom. Agent kuca, vi crtate i čitate.
fetch("/api/listings") iz features/<name>/api/ foldera. Zahtjev ima metod, putanju, header-e i, za upis, JSON tijelo.
Statusni kod (200, 201, 404, 422) i JSON. Frontend prikaže podatke ili grešku. Tri stanja: učitava se, uspjeh, greška.
docs/entities.md i types.ts kažu koja polja i koje rute postoje. Danas backend dobija zadatak da ih ispoštuje, pa frontend radi bez prepravke.
Model plus alati plus cilj. Odličan u pretvaranju jasne odluke u kod. Loš u donošenju odluke umjesto vas: pitate „kako da radim upload", dobijete fajl kroz server koji smo precrtali. Dijagram je vaš posao.
CLAUDE.md i .claude/rules/: stack sa verzijama, struktura, „svaki write traži token", „vlasnik je uvijek prijavljeni korisnik, nikad id iz tijela", „šema = migracija". Što ne zapišete, agent izmisli, uvjerljivo.
Agent kaže „gotovo" kad prestane da kuca. Vi kažete gotovo kad su pytest i ruff zeleni, /docs pokazuje rute iz entities.md, i razumijete svaku liniju.
„Napravi API za oglase." Agent izmisli: bez auth-a, id vlasnika iz tijela, upit u endpointu, create_all umjesto migracije, nijedan test za 403.
„/new-entity listings po docs/entities.md: PATCH i DELETE samo vlasnik (403), greške u obliku {detail, code, fieldErrors}, migracija, test za 401/403/404/422." Blok 1 i 5 sa dijagrama, riječ po riječ.
| Prošla subota | Danas |
|---|---|
types.ts: tipovi entiteta | Modeli i tabele u bazi, migracija |
docs/entities.md: polja, veze, pravila | Specifikacija za API i bazu, agent je čita prvu |
api/ (mock): list, get, create, update, remove | Pravi HTTP pozivi na CRUD rute; hook-ovi i ekrani netaknuti |
| Prijava (mock), zaštićene akcije | Prava prijava: /auth/login vrati JWT, backend provjerava vlasnika |
Starter sa primjerom listings: github.com/PavleDz/ai-dev-backend. „Use this template” → vaš backend repo; docs/entities.md prepišite iz svog frontend repoa.
POST /api/listingsentities.md do rute koja radiclass Listing(Base): id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String(80)) price: Mapped[float] = mapped_column(Numeric(10, 2)) owner_id: Mapped[int] = mapped_column( ForeignKey("users.id", ondelete="CASCADE")) __table_args__ = (CheckConstraint("price >= 0"),)
Što mora važiti za svaki red, važi u bazi, ne samo u Pythonu.
$ poetry run makemigrations "create listings" # pročitaj alembic/versions/<novi fajl>.py, pa: $ poetry run migrate
Šema se mijenja migracijom, nikad create_all. Migracija je git za bazu: svaki laptop i server prođu iste korake.
@router.post(path="", status_code=201) def create_listing( data: ListingCreateRequestSchema, service: Annotated[IListingService, Depends(...)], user: Annotated[CurrentUser, Depends(get_current_user)], ) -> ListingResponseSchema: # vlasnik = user.id iz tokena return service.create(data=data, current_user=user)
def test_patch_by_other_user_is_403( client, other_auth_headers, listing_id): r = client.patch(f"/api/listings/{listing_id}", json={"price": 1}, headers=other_auth_headers) assert r.status_code == 403
Za svaku rutu: srećan put, 401, 403, 404, 422. Test za tužan put je ono što agent preskoči.
Uz to repozitorijum (upiti, flush, nikad commit) i izuzeci sa kodom; sve radi /new-entity <name>: <opis>. Agent prvo predloži tabelu i pravila pristupa i čeka vašu potvrdu.
Lozinka se čuva kao bcrypt hash, nikad čista. Token važi 60 min. Frontend ga drži u localStorage i šalje kao Authorization: Bearer. Email je UNIQUE u bazi, pa 409 dolazi iz baze, ne iz if-a.
| Status | Kada | Tijelo |
|---|---|---|
| 401 | nema ili loš token, pogrešna lozinka | {"detail": "Authentication required", "code": "not_authenticated"} |
| 403 | prijavljen, ali nije vlasnik | {"detail": "Only the owner…", "code": "not_listing_owner"} |
| 404 | zapis ne postoji | {"detail": "Listing not found", "code": "listing_not_found"} |
| 409 | email već postoji | {"detail": "…", "code": "email_already_registered", "fieldErrors": {"email": "…"}} |
| 422 | neispravan unos | {"detail": "Validation failed", "code": "validation_error", "fieldErrors": {"title": "…"}} |
Frontend već zna ovaj oblik: ApiError iz lib/api-client.ts prikazuje fieldErrors ispod polja, po code odlučuje šta dalje. Najčešća laž u API-jima: sve vraća 200 sa „greška" u tekstu.
guard-bash blokira push --force, reset --hard, rm -rf app/, alembic downgrade, uv add bez pitanja. guard-edits čuva commitovane migracije i .env. verify-changes na kraju zadatka pokrene ruff + pytest: agent ne smije reći „gotovo" dok nije zeleno.
/review-changes pusti tri recenzenta odjednom: api (ugovor, greške), security (auth na write-u, vlasnik iz tokena, tajne, SQL injection), migration (tabela bez migracije). Nalaz koji nađu dva ima veću težinu. Verdikt: APPROVED / NEEDS CHANGES / BLOCKED.
decisions.md: svaka odluka ima broj (ADR-006: vlasnik iz tokena), pa recenzenti citiraju broj umjesto da raspravljaju. frequent-mistakes.md: agent drugi put pogriješi isto → jedan red ovdje; recenzenti ga čitaju prvi. Iz moje produkcije, svaki red poslije jedne greške.
pipx install poetry # jednom; Python 3.13 git clone github.com/PavleDz/ai-dev-backend.git my-team-backend cd my-team-backend && poetry install poetry run migrate poetry run seed poetry run start # :8000/docs
/brief za glavni entitet, pa /new-entity <name>: <opis> po docs/entities.md; agent predloži tabelu i pravila, vi potvrdite. Kontrolna tačka: /docs pokazuje vaše rute, poetry run test zelen.
/connect-feature <name>: mock api postaje fetch, hook-ovi netaknuti. Kontrolna tačka: lista i detalj rade sa pravim podacima i prežive osvježavanje stranice.
Login forma zove /api/auth/login, čuva token, zaštićene akcije šalju Bearer. Kontrolna tačka: tuđi zapis ne možete obrisati (403 u Network tabu).
/api-check, pa /review-changes: tri recenzenta paralelno. Ispravite nalaze visoke važnosti. git diff prije commita; hook vas ionako ne pušta sa crvenim testom.
Jedan laptop vodi sesiju sa agentom, drugi čita entities.md naglas i provjerava u /docs. Mijenjajte se poslije svakog entiteta, uz commit i push.
Ne morate razumjeti svaki red. Ovih devet znakova uočite i kao početnik.
| Znak u diffu | Zašto je problem |
|---|---|
Write endpoint bez Depends(get_current_user) | Svako može da piše. Blok 5: svaki write traži token. |
owner_id ili user_id dolazi iz tijela zahtjeva | Korisnik bira čiji je zapis. Vlasnik je uvijek iz tokena. |
SQL sklopljen od stringova (f"... WHERE id={id}") | SQL injection. Upiti idu kroz SQLAlchemy sa parametrima. |
Nova tabela, a nema fajla u alembic/versions/ | Radi samo na agentovom laptopu. Kolega i server nemaju tabelu. |
Base.metadata.create_all u kodu aplikacije | Zaobiđene migracije; sljedeća izmjena šeme nema put. |
| Lozinka, ključ ili token kao string u kodu | Tajne idu u .env, koji je u .gitignore. Agent ih zna ubaciti kad „neće da radi". |
db.commit() u repozitorijumu ili upit u api/ | Transakcija je posao endpointa (DBAPIRouter); upiti žive u repozitorijumu. Sloj pogrešan, test nemoguć. |
| Nema testa za 401, 403 i 422 | Testiran samo srećan put. Pitajte zašto. |
Novi paket u pyproject.toml koji niste tražili | Zavisnost koju niko ne poznaje. Pitajte zašto i šta bez nje. |
Lista, detalj, kreiranje i izmjena vašeg glavnog entiteta idu kroz pravi API, sa prijavom. Osvježite stranicu i podaci ostaju.
docs/entities.md, types.ts i schemas/ kažu isto. Neka contract-keeper to provjeri, i dopišite bar jedan red u frequent-mistakes.md.
Testovi i lint prolaze u CI-ju (GitHub Actions je u starteru). README kaže kako se pokreće backend. Link u Slack kanal.
docker compose up -d db, promijenite DATABASE_URL, poetry run migrate. Isti kod. Blok 1 na pravoj bazi.
Presigned princip u malom: metapodaci u bazu prije bajtova, fajl van baze. Blok 3.
Cache-aside za listu (TTL 30 s, invalidacija na upis) i rate limit na login (5 pokušaja / 15 min). Blok 6 i 8.
Martin Fowler, Software Architecture Guide (martinfowler.com/architecture). System Design Primer (GitHub). Netflix, Shopify i Instagram engineering blogovi: stvarne verzije današnjeg crteža, sa brojevima.
Kleppmann, Designing Data-Intensive Applications. Ne sad; kad prvi put budete imali problem koji rješava, tad je najbolja knjiga koju ćete pročitati.
fastapi.tiangolo.com (čita se kao knjiga) · docs.sqlalchemy.org (2.0 ORM Quick Start) · alembic.sqlalchemy.org · python-poetry.org/docs · postgresql.org/docs · redis.io/docs
code.claude.com/docs: CLAUDE.md, rules, hooks, subagenti · Context7 za ažurnu dokumentaciju biblioteka („use context7" u promptu) · starter: github.com/PavleDz/ai-dev-backend
I najvažnije: napravite tri svoja mala servisa. Prvi će biti loš, drugi bolji, treći dobar. Nema prečice.
Pitanja, pa ostatak vremena za one koji su zapeli u vježbi: rješavamo uživo, ovdje.