Frontend rzadko kończy się na komponencie i jednym fetch(). Im bliżej realnego produktu, tym częściej okazuje się, że jakość UI, czyli User Interface, to wizualna i interakcyjna warstwa produktu. zależy od tego, co dzieje się po drugiej stronie API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami.: jak backend paginuje dane, jak zwraca błędy, jak trzyma sesję, co robi przy timeoutach i czy potrafi przyjąć większy ruch.
To pierwsza część serii Backend dla frontendowca. Zaczynamy od fundamentów: architektury aplikacji webowej, serwera aplikacji, baz danych, kontraktu API i CORS. To są tematy, które frontendowiec najczęściej dotyka jako pierwszy, nawet jeśli formalnie nie pisze backendu na co dzień.
Nie musisz od razu stawać się backendowcem. Warto jednak znać język, którym posługuje się druga strona zespołu. Dzięki temu szybciej zrozumiesz, dlaczego formularz zwraca 422, czemu lista ładuje się pięć sekund, skąd biorą się problemy z CORS-em i dlaczego „to tylko jeden endpoint” czasem oznacza kilka dni pracy.
Krótka odpowiedź: jeśli chcesz mieć solidny fundament, zacznij od HTTP request-response, status codes, REST API to styl projektowania interfejsów oparty na zasobach, metodach HTTP i bezstanowej komunikacji., paginacji, strukturalnych błędów, CORS, bazy danych i podstaw projektowania endpointów. W praktyce dobry start to Node.js + Express albo Hono, PostgreSQL, Prisma lub Drizzle, walidacja Zod i świadome rozumienie tego, co frontend dostaje z API.
Szersza perspektywa — architektura aplikacji webowej
Zanim wejdziemy w szczegóły, warto zobaczyć cały obraz. Dla użytkownika aplikacja jest jednym ekranem w przeglądarce. Dla systemu to zestaw współpracujących warstw: frontend wysyła request, load balancer kieruje go do jednego z serwerów API, serwer rozmawia z bazą, cache i storage, a potem wszystko wraca do UI jako odpowiedź.
To oczywiście uproszczenie, ale przydatne. Większość problemów, które frontend widzi jako „wolny ekran”, „dziwny błąd” albo „czasem działa, czasem nie”, dzieje się właśnie na styku tych warstw.
1. Serwer aplikacji jest sercem backendu
Serwer aplikacji to miejsce, w którym request przestaje być samym adresem URL, a zaczyna być decyzją biznesową. To tutaj sprawdzasz, czy użytkownik może wykonać akcję, walidujesz dane, zapisujesz coś w bazie i decydujesz, jaką odpowiedź powinien dostać frontend.
- Nasłuchuje na porcie (np. 3000)
- Przyjmuje HTTP requesty
- Przetwarza logikę biznesową
- Zwraca odpowiedzi
Popularne opcje
Node.js + Express to najczęstszy punkt wejścia dla osób z frontendu. Zostajesz w JavaScripcie, ale przenosisz część logiki na serwer:
Inne popularne (JS / TS):
- Fastify — znacznie szybsza alternatywa dla Expressa, która oferuje świetne wsparcie dla walidacji opartych na schematach (schema-first).
- Hono — lekki i ultraszybki framework stworzony z myślą o środowiskach typu edge (jak Cloudflare Workers czy Vercel Edge), ale doskonale radzący sobie również w klasycznym Node czy Bunie.
- NestJS — potężny, mocno ustrukturyzowany framework (tzw. opinionated), którego architektura mocno przypomina Angulara. To idealny wybór do skomplikowanych projektów i dla większych zespołów.
- Bun — to właściwie nie framework, a cały nowy, niezwykle szybki runtime (alternatywa dla Node.js). Posiada wbudowanego bundlera, test runnera i jest w dużym stopniu kompatybilny z API Node'a.
- Deno — kolejny nowoczesny runtime, stworzony przez twórcę Node.js. Natywnie wspiera TypeScript, z definicji stawia na bezpieczeństwo (mechanizm secure-by-default) i posiada świetnie przemyślaną bibliotekę standardową.
Pozostałe języki:
- Python: Django, FastAPI, Flask,
- Ruby: Ruby on Rails,
- Go: Gin, Echo,
- Java: Spring Boot,
- PHP: Laravel,
- .NET: ASP.NET Core.
Co robi serwer aplikacji?
W najprostszej wersji serwer aplikacji jest tylko routerem i warstwą pośrednią między frontendem a bazą. W realnych projektach bardzo szybko dochodzą kolejne odpowiedzialności:
- Routing — mapowanie URL → kod
- Middleware — przetwarzanie requestów (auth, logging, CORS)
- Logika biznesowa — walidacja, obliczenia, reguły
- Komunikacja z bazą — queries, ORM
- Odpowiedzi — formatowanie JSON, status codes
2. Bazy danych — gdzie żyją dane
Baza danych jest tym elementem backendu, którego najłatwiej nie docenić na początku. Dopóki masz kilka rekordów, wszystko wydaje się proste. Problemy zaczynają się, gdy dochodzą relacje, filtry, indeksy, migracje, transakcje i rosnąca liczba użytkowników. Jeśli chcesz wejść głębiej w sam wybór typu bazy, osobno opisałem to w tekście SQL vs NoSQL dla początkujących.
SQL (relacyjne)
Dane w tabelach z relacjami:
Kiedy SQL: e-commerce, finanse, CRM, panele administracyjne i większość klasycznych aplikacji biznesowych. Jeżeli dane mają relacje i potrzebujesz spójności, SQL jest bezpiecznym domyślnym wyborem.
Popularne: PostgreSQL, MySQL, SQLite
NoSQL (nierelacyjne)
Dokumenty JSON-like:
Kiedy NoSQL: social media, real-time apps, dane dokumentowe lub bardzo nierówna struktura. To dobry wybór wtedy, gdy model danych bardziej przypomina dokument JSON niż zestaw stabilnych tabel.
Popularne: MongoDB, Firebase, DynamoDB
ORM — abstrakcja nad bazą
ORM albo query builder pozwala pisać zapytania w kodzie aplikacji, zamiast składać SQL ręcznie w każdym miejscu. To wygodne, bo dostajesz typowanie, migracje i mniej powtarzalnego boilerplate'u. Nadal warto jednak rozumieć SQL, bo ORM nie zwalnia z myślenia o indeksach, JOIN-ach i kosztach zapytań.
Popularne ORM/query buildery: Prisma, Drizzle, Kysely, TypeORM (Node), SQLAlchemy (Python), Hibernate (Java), Eloquent (Laravel), Active Record (Rails).
Indeksy — szybkie wyszukiwanie
Indeks działa trochę jak spis treści w książce. Bez niego baza musi przejrzeć wiele wierszy, żeby znaleźć właściwy rekord. Z indeksem może przejść od razu do konkretnego miejsca. Dlatego zapytanie SELECT * FROM users WHERE email = '...' bez indeksu skanuje całą tabelę, a z indeksem na kolumnie email jest nieporównywalnie tańsze:
Kilka zasad wystarczy na start:
- Indeksuj kolumny używane w
WHERE,JOIN,ORDER BY - Composite index na
(user_id, created_at)przyspiesza listę postów usera posortowaną po dacie - Każdy indeks kosztuje pamięć i spowalnia
INSERT/UPDATE— nie indeksuj na zapas EXPLAIN ANALYZEpokazuje, czy query używa indeksu
Transakcje — wszystko albo nic
Transakcja jest potrzebna wtedy, gdy kilka operacji musi wydarzyć się jako jedna całość. Najprostszy przykład to przelew: -100 z konta A i +100 na konto B. Jeżeli druga operacja się wywali, pierwsza nie może zostać w bazie.
Jeśli druga operacja zawiedzie (timeout, błąd walidacji), pierwsza zostanie automatycznie cofnięta (ROLLBACK). Bez transakcji można skończyć z pieniędzmi, które wyparowały.
ACID — fundamentalne właściwości transakcji w SQL: Atomicity, Consistency, Isolation, Durability.
Problem N+1 — pułapka, którą widzi frontend
Problem N+1 jest świetnym przykładem błędu backendowego, który frontend odczuwa bardzo wyraźnie. Kod wygląda niewinnie: pobierasz listę, a potem dla każdego elementu dociągasz autora. Przy małej liczbie rekordów działa. Przy większej liczbie zaczyna zabijać czas odpowiedzi.
Dla 100 postów to różnica między 1 a 101 zapytaniami do bazy. Frontend widzi tylko efekt: spinner trwa za długo, lista pojawia się po chwili, użytkownik zaczyna klikać drugi raz. Źródło problemu siedzi jednak w sposobie pobierania danych.
Connection pool — recykling połączeń
Każde otwarcie połączenia do bazy (TCP + handshake + auth) trwa kilkanaście milisekund. Aplikacja utrzymuje pool gotowych połączeń (np. 10), które są współdzielone między requestami.
Pułapka serverless: Lambda / Vercel Functions tworzą nowe instancje per request — pool nie zostaje zachowany między wywołaniami i baza może padać pod obciążeniem (too many connections). Rozwiązania: pgbouncer, Prisma Accelerate, Neon (built-in pooler), Supavisor (Supabase), serverless drivery (@neondatabase/serverless, @vercel/postgres).
Migracje — wersjonowanie schematu
Schemat bazy też jest częścią aplikacji. Dodanie kolumny, zmiana typu albo usunięcie tabeli nie powinny być ręczną akcją „klikniętą na produkcji”. Migracje zapisują te zmiany jako kod, który trafia do repozytorium i może zostać wykonany w tej samej kolejności na każdym środowisku.
Każde środowisko (dev/staging/prod) wykonuje te same migracje w tej samej kolejności. Reguła: backward-compatible najpierw — najpierw dodaj kolumnę, potem deploy kodu, potem ewentualne usunięcie starej.
3. API — umowa między frontem a backendem
API nie jest tylko technicznym endpointem. To kontrakt między zespołami i warstwami aplikacji. Jeśli kontrakt jest niejasny, frontend zaczyna zgadywać: czy pusta tablica oznacza brak danych, czy błąd? Czy 400 to walidacja, czy zepsuty JSON? Czy można retry'ować POST? Dobre API usuwa takie pytania.
REST API
REST to najczęściej spotykane podejście, bo dobrze mapuje się na HTTP: zasoby mają adresy, a akcje są opisane metodami.
GraphQL
GraphQL przesuwa część kontroli do klienta. Frontend nie dostaje jednej z góry ustalonej odpowiedzi, tylko sam deklaruje, jakie pola są mu potrzebne:
To jest bardzo wygodne przy produktach, w których różne widoki potrzebują różnych kształtów danych. Cena to większa złożoność po stronie serwera, cache i obserwowalności.
tRPC
tRPC jest najbardziej atrakcyjne wtedy, gdy cały stack jest w TypeScripcie. Zamiast ręcznie synchronizować typy requestów i odpowiedzi, frontend korzysta z typów wyprowadzonych bezpośrednio z backendu.
Status codes — które widzi frontend
Status code to pierwsza informacja, jaką frontend dostaje z API. Jeżeli wszystko poza 200 traktujesz tak samo, tracisz dużo kontekstu. 401, 403, 422 i 429 wymagają zupełnie różnych reakcji w UI.
| Kod | Znaczenie | Kiedy |
|---|---|---|
| 200 OK | Sukces | GET / PUT z odpowiedzią |
| 201 Created | Utworzono | POST tworzący zasób (zwróć URL nowego zasobu w Location) |
| 204 No Content | Sukces, brak ciała | DELETE bez odpowiedzi |
| 301 / 308 | Permanent redirect | URL zasobu się zmienił |
| 302 / 307 | Temporary redirect | Tymczasowe przekierowanie |
| 304 Not Modified | Cache jest aktualny | Po If-None-Match z ETag |
| 400 Bad Request | Klient wysłał śmieci | JSON się nie sparsował |
| 401 Unauthorized | Brak / zła autentykacja | Zaloguj się ponownie |
| 403 Forbidden | Zalogowany, ale brak uprawnień | Pokaż komunikat, nie redirect na login |
| 404 Not Found | Brak zasobu | |
| 409 Conflict | Konflikt stanu | Email już zajęty, optimistic lock |
| 410 Gone | Zasób usunięty na stałe | |
| 422 Unprocessable Entity | Walidacja nie przeszła | Pokaż błędy per pole |
| 429 Too Many Requests | Rate limit | Sprawdź Retry-After, cofnij się |
| 500 Internal Server Error | Wina backendu | Powiedz "spróbuj ponownie" |
| 502 / 503 / 504 | Gateway / unavailable / timeout | Infra padła, retry z backoff |
Struktura odpowiedzi błędów
Dobre API nie zwraca tylko stringa w stylu "Something went wrong". Frontend potrzebuje informacji, czy ma pokazać błąd pod konkretnym polem, przekierować do logowania, odczekać chwilę, czy dać użytkownikowi możliwość ponowienia akcji. Do tego służą strukturalne błędy. Standardem branżowym jest RFC 9457 Problem Details:
Dzięki temu frontend wie dokładnie:
- Który status dostał (decyzja: retry / login / pokaż błąd)
- Co dokładnie się stało (
title,detail) - Które pola są nieprawidłowe (mapowanie na komunikaty pod inputami)
- Jaki kod błędu (do logiki w UI — np.
INSUFFICIENT_FUNDSvsRATE_LIMITED)
Nigdy nie zwracaj stack trace w produkcji — to wektor ataku.
Paginacja — listy nigdy bez paginacji
Lista bez paginacji jest jednym z tych błędów, które długo pozostają niewidoczne. Przy 30 rekordach wszystko działa. Przy 30 000 rekordów endpoint zaczyna mielić, UI się zacina, a baza robi więcej pracy, niż powinna. Najczęściej spotkasz dwa wzorce:
Offset / limit — proste, intuicyjne, ale wolne na dużych zbiorach (baza musi pominąć N wierszy):
Cursor-based — szybkie, stabilne (nowe elementy nie psują stronicowania), używane przez Twitter, Stripe, GitHub:
Cursor to nieprzeźroczysty token (zwykle base64) oparty na id lub created_at ostatniego elementu strony.
Wybór: offset dla admin paneli z numeracją stron, cursor dla feedów (timeline, infinite scroll).
Filtrowanie i sortowanie
Filtrowanie i sortowanie powinny mieć przewidywalną konwencję. Dzięki temu frontend nie musi uczyć się osobnego stylu dla każdego endpointu.
status=published— filtersort=-created_at— minus = malejącofields=id,title— sparse fieldsets, oszczędzają transfer
Idempotency — bezpieczne retry POST
Frontend często musi ponowić request: użytkownik ma słaby internet, serwer odpowiada za wolno, po drodze pojawia się timeout. Przy GET zwykle nie ma problemu. Przy POST możesz przypadkiem stworzyć drugie zamówienie albo podwójną płatność. Idempotency key daje backendowi sposób na rozpoznanie, że to ta sama próba, a nie nowa operacja.
Backend:
- Zapisuje
key+ odpowiedź w bazie / Redis (TTL 24h) - Drugi request z tym samym kluczem zwraca tę samą odpowiedź, zamiast tworzyć nowy zasób
- Stripe, GitHub, Shopify używają tego standardowo
GET / PUT / DELETE są idempotentne z definicji. POST i PATCH — nie, dlatego potrzebują klucza.
Wersjonowanie API
API żyje razem z produktem. Dzisiaj pole name wystarcza, jutro pojawia się displayName, za miesiąc dochodzi mobile app, a za pół roku integracja partnerska. Wersjonowanie jest sposobem na zmianę kontraktu bez rozbijania istniejących klientów.
W URL (proste, najczęstsze):
W headerze (czystsze URL):
Zero versioning (zawsze backward-compatible):
- Nigdy nie usuwasz pól
- Nigdy nie zmieniasz znaczenia
- Nowe rzeczy idą w nowych polach
Stara wersja działa równolegle przez okres deprecation. Przy odpowiedziach z deprecated endpointów warto wysyłać header:
BFF — Backend for Frontend
BFF, czyli Backend for Frontend, to wzorzec, w którym frontend ma „swój” backend dopasowany do potrzeb konkretnego UI. Nie chodzi o dokładanie warstwy dla sportu. Chodzi o sytuacje, w których ekran musi zebrać dane z kilku usług, ukryć sekrety albo przekształcić odpowiedź pod konkretny widok. W Next.js route handlers i server actions często pełnią właśnie tę rolę:
Plusy:
- Ukrywa złożoność backendu — frontend nie musi wołać 5 serwisów
- Mniej requestów z przeglądarki — jeden call zamiast pięciu
- Trzyma sekrety po stronie serwera — API keys nie wyciekają do klienta
- Transformacje per-platform — inne API dla web, inne dla mobile
Minus: dodatkowa warstwa do utrzymania, łatwo o nadużycie (BFF puchnie do całego backendu).
4. CORS — dlaczego "from origin" wkurza frontendowca
CORS jest frustrujący, bo wygląda jak błąd backendu, a tak naprawdę zaczyna się w przeglądarce. Browser domyślnie blokuje requesty między różnymi origin'ami (https://app.example.com → https://api.example.com). To element modelu bezpieczeństwa weba, czyli Same-Origin Policy.
Jak to działa
Dla prostych requestów (GET / HEAD / POST z Content-Type: application/x-www-form-urlencoded | multipart/form-data | text/plain) przeglądarka wysyła request od razu. Dopiero gdy dostanie odpowiedź, sprawdza, czy backend pozwolił ją udostępnić frontendowi:
Jeśli nagłówka brakuje albo origin się nie zgadza, przeglądarka odrzuca odpowiedź. Request faktycznie poszedł do serwera, ale JavaScript w UI nie dostanie danych.
Dla skomplikowanych requestów (POST z Content-Type: application/json, custom headers, PUT / DELETE / PATCH) przeglądarka najpierw robi preflight — request OPTIONS:
Backend musi odpowiedzieć:
Dopiero po sukcesie preflight przeglądarka wysyła właściwy request. Każdy preflight to dodatkowy round-trip — Max-Age cachuje odpowiedź na X sekund.
Konfiguracja w Express
Typowe pułapki
Access-Control-Allow-Origin: *nie działa zcredentials: 'include'. Musisz wskazać konkretny origin.- Cookies nie polecą bez:
credentials: 'include'po stronie frontu iAccess-Control-Allow-Credentials: truepo stronie backendu. - Localhost vs production — najczęstszy bug: środowisko dev ma inne origin niż prod. Konfiguracja per env.
- Subdomain matters —
app.example.comiapi.example.comto różne origin'y. - Port matters —
localhost:3000≠localhost:5173.
CORS to nie autoryzacja
CORS chroni jedynie przeglądarkę użytkownika przed cross-site requestami. Curl, aplikacja mobilna i request server-side mogą wołać Twoje API bez oglądania się na CORS. Dlatego CORS nie zastępuje autoryzacji. Token, sesja i kontrola uprawnień są osobną warstwą.
Co dalej w serii?
Ta część daje fundament: wiesz, z czego składa się typowy backend, jak rozmawia z bazą i jak projektować API, które frontend może obsłużyć bez zgadywania.
W kolejnych częściach przechodzimy do tematów, które pojawiają się, gdy aplikacja zaczyna żyć: komunikacja w czasie rzeczywistym, integracje, logowanie, cache, deployment, monitoring i bezpieczeństwo.
- Backend dla frontendowca: auth, real-time i integracje
- Backend dla frontendowca: cache, deployment i bezpieczeństwo
