StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

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

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.

SEO & Performance

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

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

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

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

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

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.

SEO & Performance

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

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

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

Astro

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

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.

SEO & Performance

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

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

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

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
  • Audyt SEO i Performance
  • Testy automatyczne i QA
  • Konsultacje Produktowe
  • Automatyzacja Procesów AI
  • 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
25 maja 2026 10:55

Frontend rzadko kończy się na komponencie i jednym fetch(). Im bliżej realnego produktu, tym częściej okazuje się, że jakość UI zależy od tego, co dzieje się po drugiej stronie API: jak backend paginuje dane, jak zwraca błędy, jak trzyma sesję, co robi przy timeoutach i czy potrafi przyjąć większy ruch.

Artykuł 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

To pierwsza część serii Backend dla frontendowca. Zaczynamy od fundamentów: architektury aplikacji webowej, serwera aplikacji, baz danych, kontraktu API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami. Tu chodzi o konwencje plików Next.js, które zamieniają eksportowaną funkcję w gotowy plik sitemap.xml lub robots.txt. i CORS (Cross-Origin Resource Sharing) to mechanizm przeglądarki kontrolujący, czy strona z jednej domeny może wysyłać żądania do API na innej domenie.. 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.

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 częsty 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 zobaczyć, jak w praktyce podpiąć serwerową bazę Postgres do Next.js, sprawdź tekst Neon Postgres + Next.js — serverless baza danych, która startuje w milisekundach.

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

Werdykt Labu

Serwer, baza danych i API to fundament, na którym stoi każda aplikacja webowa. Frontendowiec, który rozumie status codes, strukturę błędów, paginację i problem N+1, przestaje zgadywać dlaczego lista ładuje się pięć sekund albo skąd bierze się podwójne zamówienie po timeoucie.

Nie trzeba od razu pisać backendu. Wystarczy wiedzieć, gdzie leżą granice odpowiedzialności, jak rozmawiać z drugą stroną zespołu i dlaczego „to tylko jeden endpoint" może kryć migrację, indeks i decyzję o paginacji — to redukuje tygodniowe sprinty do dni i błędy produkcyjne do historii o tym, że kiedyś coś poszło nie tak.

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

Pozostałe części serii

  • Backend dla frontendowca: auth, real-time i integracje
  • Backend dla frontendowca: cache, deployment i bezpieczeństwo
  • 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
  • Werdykt Labu1 min
  • Pozostałe części serii1 min

Często zadawane pytania

Źródła i dokumentacjaZweryfikowano: 13 maja 2026

Materiały wykorzystane do weryfikacji artykułu „Backend dla frontendowca: serwer, bazy danych i API”:

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.

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
Maciej Sala

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty side-projecty.

Moje artykułyWięcej o mnie

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

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

Redis, cache HTTP, OWASP i RODO — część backendowej wiedzy, której frontendowiec unika jak ognia. Dlaczego warto jednak ją mieć i od czego zacząć?

Maciej Sala

Maciej Sala

Founder Strivelab

30 lipca 2025
REST API — zasady projektowania i dobre praktyki
REST API — zasady projektowania i dobre praktyki

REST API zaprojektowane naprędce wróci do Ciebie z długiem. Konwencje, wersjonowanie i obsługa błędów — zasady, których tutoriale zwykle pomijają.

Maciej Sala

Maciej Sala

Founder Strivelab

5 grudnia 2025
REST API WordPressa — integracja z React i Next.js
REST API WordPressa — integracja z React i Next.js

WordPress REST API z Next.js — autentykacja, custom endpointy i cache bez typowych pułapek, w które wpada każdy, kto robi to pierwszy raz.

Maciej Sala

Maciej Sala

Founder Strivelab

29 stycznia 2025
Poprzedni wpisNext.js a SEO — kiedy naprawdę daje przewagę nad zwykłym ReactemNext.js naprawdę poprawia SEO — ale nie samo z siebie. Kiedy SSR i SSG dają realną przewagę nad zwykłym Reactem i kiedy ta różnica jest pozorna?
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