StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN
StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

QA & Automation

Testy automatyczne komponentów i E2E w oparciu o Cypress.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • SEO & Performance Sprint
  • QA & Stabilizacja
  • Konsultacje Product / Delivery
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
Backend

Backend dla frontendowca: serwer, bazy danych i API

Pierwsza część serii Backend dla frontendowca: architektura aplikacji, serwer, bazy danych, API, status code, paginacja, idempotency, BFF i CORS.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
28 lipca 2025 11:45
Czytanie
13 min czytania
Aktualizacja
30 kwietnia 2026 19:50

W skrócie

  • Status codes mają znaczenie — 401, 403, 422 i 429 wymagają zupełnie różnych reakcji UI; traktowanie wszystkiego poza 200 tak samo oznacza utratę cennego kontekstu z backendu
  • Strukturalne błędy (RFC 9457) — dobre API zwraca nie tylko status, ale też typ błędu, szczegółowy opis i mapę błędów per pole formularza, co pozwala frontendowi pokazać komunikaty dokładnie przy właściwych inputach
  • Paginacja — zawsze — lista bez paginacji działa przy 30 rekordach, zabija bazę przy 30 000; cursor-based jest szybsze i stabilniejsze niż offset/limit przy feedach i infinite scroll
  • Problem N+1 — pobieranie autora dla każdego posta osobną pętlą to N+1 zapytań do bazy zamiast jednego JOIN-a; frontend widzi efekt jako wolny spinner, choć problem leży w warstwie pobierania danych
  • Idempotency key — unikalny klucz w nagłówku POST chroni przed duplikatami przy retry i timeoutach; kluczowe przy płatnościach i zamówieniach, gdzie ponowna operacja nie może oznaczać podwójnej akcji
  • CORS to nie autoryzacja — przeglądarka respektuje nagłówki CORS, ale curl i requesty server-side je ignorują; autoryzacja tokenem lub sesją to osobna, obowiązkowa warstwa

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.

Top tip

Frontendowiec nie musi pisać całego backendu, żeby projektować lepszy UI. Wystarczy rozumieć kontrakt API, statusy błędów, paginację i ograniczenia danych, bo to one najczęściej wyznaczają realne stany interfejsu.

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ź.

Diagram
Podstawowa architektura aplikacji webowej

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:

Code
import express from 'express'
 
const app = express()
 
app.get('/api/users', async (req, res) => {
  const users = await db.query('SELECT * FROM users')
  res.json(users)
})
 
app.listen(3000)

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:

  1. Routing — mapowanie URL → kod
  2. Middleware — przetwarzanie requestów (auth, logging, CORS)
  3. Logika biznesowa — walidacja, obliczenia, reguły
  4. Komunikacja z bazą — queries, ORM
  5. 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:

Code
-- Tabele
users (id, name, email)
posts (id, user_id, title, content)
 
-- Relacja przez user_id
SELECT users.name, posts.title
FROM users
JOIN posts ON users.id = posts.user_id

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:

Code
{
  "_id": "abc123",
  "name": "Jan",
  "posts": [
    { "title": "Post 1" },
    { "title": "Post 2" }
  ]
}

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ń.

Code
// Prisma
const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: { posts: true },
})
 
// Zamiast:
// SELECT * FROM users
// LEFT JOIN posts ON users.id = posts.user_id
// WHERE users.id = 1

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:

Code
CREATE INDEX idx_users_email ON users(email);
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);

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 ANALYZE pokazuje, 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.

Code
await prisma.$transaction(async (tx) => {
  await tx.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 100 } },
  })
  await tx.account.update({
    where: { id: 2 },
    data: { balance: { increment: 100 } },
  })
})

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.

Code
// ❌ N+1: 1 zapytanie + N zapytań po jednym dla każdego posta
const posts = await prisma.post.findMany()
for (const post of posts) {
  post.author = await prisma.user.findUnique({
    where: { id: post.authorId },
  })
}
 
// ✅ 1 zapytanie z JOINem
const posts = await prisma.post.findMany({
  include: { author: true },
})

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.

Code
// pg pool config
import { Pool } from 'pg'
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // max połączeń
  idleTimeoutMillis: 30000, // zamknij idle po 30s
})

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.

Code
prisma migrate dev --name add_user_role
Code
-- 20260101_add_user_role/migration.sql
ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'user';

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.

Code
GET    /api/users        → lista userów
GET    /api/users/123    → user o id 123
POST   /api/users        → utwórz usera
PUT    /api/users/123    → aktualizuj usera
DELETE /api/users/123    → usuń usera

GraphQL

GraphQL przesuwa część kontroli do klienta. Frontend nie dostaje jednej z góry ustalonej odpowiedzi, tylko sam deklaruje, jakie pola są mu potrzebne:

Code
query {
  user(id: 123) {
    name
    email
    posts {
      title
    }
  }
}

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.

Code
// Backend
export const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(({ input }) => {
      return db.user.findUnique({ where: { id: input.id } })
    }),
})
 
// Frontend — pełne typowanie
const user = await trpc.getUser.query({ id: 123 })

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.

KodZnaczenieKiedy
200 OKSukcesGET / PUT z odpowiedzią
201 CreatedUtworzonoPOST tworzący zasób (zwróć URL nowego zasobu w Location)
204 No ContentSukces, brak ciałaDELETE bez odpowiedzi
301 / 308Permanent redirectURL zasobu się zmienił
302 / 307Temporary redirectTymczasowe przekierowanie
304 Not ModifiedCache jest aktualnyPo If-None-Match z ETag
400 Bad RequestKlient wysłał śmieciJSON się nie sparsował
401 UnauthorizedBrak / zła autentykacjaZaloguj się ponownie
403 ForbiddenZalogowany, ale brak uprawnieńPokaż komunikat, nie redirect na login
404 Not FoundBrak zasobu
409 ConflictKonflikt stanuEmail już zajęty, optimistic lock
410 GoneZasób usunięty na stałe
422 Unprocessable EntityWalidacja nie przeszłaPokaż błędy per pole
429 Too Many RequestsRate limitSprawdź Retry-After, cofnij się
500 Internal Server ErrorWina backenduPowiedz "spróbuj ponownie"
502 / 503 / 504Gateway / unavailable / timeoutInfra 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:

Code
{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "Request body contains invalid fields",
  "instance": "/api/users",
  "errors": {
    "email": "Niepoprawny format",
    "password": "Min. 8 znaków"
  }
}

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_FUNDS vs RATE_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):

Code
GET /api/posts?page=2&limit=20
Code
{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 1547,
    "totalPages": 78
  }
}

Cursor-based — szybkie, stabilne (nowe elementy nie psują stronicowania), używane przez Twitter, Stripe, GitHub:

Code
GET /api/posts?cursor=eyJpZCI6MTIzfQ&limit=20
Code
{
  "data": [...],
  "nextCursor": "eyJpZCI6MTAzfQ",
  "hasMore": true
}

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.

Code
GET /api/posts?status=published&author=42&sort=-created_at&fields=id,title
  • status=published — filter
  • sort=-created_at — minus = malejąco
  • fields=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.

Code
fetch('/api/payments', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({ amount: 100 }),
})

Backend:

  1. Zapisuje key + odpowiedź w bazie / Redis (TTL 24h)
  2. Drugi request z tym samym kluczem zwraca tę samą odpowiedź, zamiast tworzyć nowy zasób
  3. 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):

Code
/api/v1/users
/api/v2/users

W headerze (czystsze URL):

Code
GET /api/users
Accept: application/vnd.example.v2+json

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:

Code
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT

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ę:

Code
// app/api/dashboard/route.ts — agreguje 3 mikroserwisy w jednym call
export async function GET() {
  const [user, orders, notifications] = await Promise.all([
    fetch(`${USER_SERVICE}/me`, { headers: serverAuth }),
    fetch(`${ORDER_SERVICE}/recent`, { headers: serverAuth }),
    fetch(`${NOTIF_SERVICE}/unread`, { headers: serverAuth }),
  ])
 
  return Response.json({
    user: await user.json(),
    orders: await orders.json(),
    notifications: await notifications.json(),
  })
}

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:

Code
Access-Control-Allow-Origin: https://app.example.com

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:

Code
OPTIONS /api/users
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

Backend musi odpowiedzieć:

Code
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

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

Code
import cors from 'cors'
 
app.use(
  cors({
    origin: ['https://app.example.com', 'http://localhost:3000'],
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
  }),
)

Typowe pułapki

  • Access-Control-Allow-Origin: * nie działa z credentials: 'include'. Musisz wskazać konkretny origin.
  • Cookies nie polecą bez: credentials: 'include' po stronie frontu i Access-Control-Allow-Credentials: true po stronie backendu.
  • Localhost vs production — najczęstszy bug: środowisko dev ma inne origin niż prod. Konfiguracja per env.
  • Subdomain matters — app.example.com i api.example.com to 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

Źródła i dokumentacja

  • MDN: An overview of HTTP
  • MDN: CORS
  • PostgreSQL Tutorial
  • Use The Index, Luke! — SQL performance
  • Prisma docs
  • REST API design — Microsoft
  • RFC 9457: Problem Details for HTTP APIs
  • Stripe API design — postmortem

Często zadawane pytania

Najlepszy punkt startowy to Node.js + Express albo Hono — JavaScript, który już znasz, po stronie serwera. Naucz się przyjmować HTTP requesty, czytać parametry i body, zwracać JSON z odpowiednim status code. Następnie dodaj bazę danych (PostgreSQL + Prisma lub Drizzle), paginację, walidację i strukturalne błędy. Nie próbuj uczyć się wszystkiego naraz — zbuduj jedno działające API end-to-end zanim przejdziesz dalej.

Pracuję z tym zawodowo.

Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.

Skontaktuj się ze mną
Maciej Sala

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Moje artykułyWięcej o mnie

Seria

Backend dla frontendowca
Część 1 / 3
  1. Backend dla frontendowca: serwer, bazy danych i API
  2. 2Backend dla frontendowca: auth, real-time i integracje
  3. 3Backend dla frontendowca: cache, deployment i bezpieczeństwo
Poprzedni wpisNext.js a SEO — kiedy naprawdę daje przewagę nad zwykłym ReactemJak Next.js wpływa na SEO w praktyce? SSR, SSG, metadata, Core Web Vitals i techniczne ograniczenia, o których warto wiedzieć przed wyborem frameworka.
Maciej Sala

Maciej Sala

Founder Strivelab

15 lipca 2025
Następny wpisBackend dla frontendowca: auth, real-time i integracjeDruga część serii Backend dla frontendowca: SSE, WebSockets, polling, webhooki, sesje, JWT, cookies, CSRF, refresh token rotation i MFA.
Maciej Sala

Maciej Sala

Founder Strivelab

29 lipca 2025

Spis treści

7 sekcji · 15 min

  • Szersza perspektywa — architektura aplikacji webowej1 min
  • 1. Serwer aplikacji jest sercem backendu2 min
  • 2. Bazy danych — gdzie żyją dane3 min
  • 3. API — umowa między frontem a backendem5 min
  • 4. CORS — dlaczego "from origin" wkurza frontendowca2 min
  • Co dalej w serii?1 min
  • Źródła i dokumentacja1 min

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Backend dla frontendowca: cache, deployment i bezpieczeństwo

Backend dla frontendowca: cache, deployment i bezpieczeństwo

Trzecia część serii Backend dla frontendowca: Redis, HTTP cache, kolejki, file storage, deployment, CI/CD, monitoring, OWASP, rate limiting i RODO.

Maciej Sala

Maciej Sala

Founder Strivelab

30 lipca 2025
REST API — zasady projektowania i dobre praktyki

REST API — zasady projektowania i dobre praktyki

Praktyczny przewodnik po projektowaniu REST API. Konwencje URL, metody HTTP, błędy, wersjonowanie, paginacja i kilka ważnych niuansów, które zwykle pomija się w prostych tutorialach.

Maciej Sala

Maciej Sala

Founder Strivelab

5 grudnia 2025
Backend dla frontendowca: auth, real-time i integracje

Backend dla frontendowca: auth, real-time i integracje

Druga część serii Backend dla frontendowca: SSE, WebSockets, polling, webhooki, sesje, JWT, cookies, CSRF, refresh token rotation i MFA.

Maciej Sala

Maciej Sala

Founder Strivelab

29 lipca 2025