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, statusy HTTP, paginacja, idempotency, BFF i CORS.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
28 lipca 2025 11:45
Czytanie
16 min czytania
Aktualizacja
11 czerwca 2026 10:00

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

w skrócie

  • 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 () — 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.
  • — 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 i . 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, ale 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 „tylko jeden endpoint” czasem oznacza kilka dni pracy.

Wskazówka

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, ale już 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 jako odpowiedź.

Diagram
Podstawowa architektura aplikacji webowej

To oczywiście uproszczenie, ale przydatne, ponieważ większość problemów, które frontend widzi jako „wolny ekran” albo „dziwny błąd”, dzieje się właśnie na styku tych warstw.

1. Serwer aplikacji jest sercem backendu

W serwerze aplikacji 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ą — zapytania, ORM.
  5. Odpowiedzi — formatowanie JSON, statusy HTTP.

2. Bazy danych — tam, 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 jest 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ź artykuł o Neon Postgres i Next.js — serverless bazie 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 : 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 użytkownika posortowaną po dacie
  • Każdy indeks kosztuje pamięć i spowalnia INSERT/UPDATE — nie indeksuj na zapas
  • EXPLAIN ANALYZE pokazuje, czy zapytanie 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ę nie powiedzie, wtedy 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 pozornie wygląda niewinnie: pobierasz listę, a potem dla każdego elementu dociągasz autora. Przy małej liczbie rekordów jakoś działa, ale 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 JOIN-em
const postsWithAuthors = await prisma.post.findMany({
  include: { author: true },
})

Dla 100 postów to różnica między jednym a 101 zapytaniami do bazy, a frontend odczuwa tylko skutki. Spinner trwa za długo, lista pojawia się z opóźnieniem, a zniecierpliwiony użytkownik zaczyna klikać ponownie. Choć problem objawia się w interfejsie, jego prawdziwe źródło tkwi w nieefektywnym sposobie pobierania danych.

Connection pool, czyli 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 : Lambda / Vercel Functions mogą ponownie używać instancji, ale nie możesz na tym polegać. Przy większym ruchu platforma uruchamia wiele instancji równolegle, każda z własnym poolem, i baza może paść 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, ponieważ 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 użytkowników
GET    /api/users/123    → użytkownik o id 123
POST   /api/users        → utwórz użytkownika
PUT    /api/users/123    → aktualizuj użytkownika
DELETE /api/users/123    → usuń użytkownika

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. Dzieje się to za cenę większej złożoności 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 })

Statusy HTTP — które widzi frontend

Status HTTP 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łe uwierzytelnienieZaloguj 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", ponieważ 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.

Walidacja inputu — nie ufaj frontendowi

Frontend może walidować formularz dla wygody użytkownika, ale backend musi walidować dane ponownie. Użytkownik może ominąć UI, wysłać request przez curl, zmodyfikować payload w DevTools albo użyć starej wersji aplikacji. Dlatego właśnie walidacja musi stać na granicy API.

Typowy przepływ:

  1. Parsujesz JSON i sprawdzasz Content-Type.
  2. Walidujesz kształt danych schematem.
  3. Walidujesz reguły biznesowe.
  4. Zwracasz 422 z mapą błędów per pole.

Przykład z Zod:

Code
import { z } from 'zod'
 
const createUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2).max(80),
})
 
app.post('/api/users', async (req, res) => {
  const result = createUserSchema.safeParse(req.body)
 
  if (!result.success) {
    return res.status(422).json({
      type: 'https://api.example.com/errors/validation',
      title: 'Validation failed',
      status: 422,
      errors: result.error.flatten().fieldErrors,
    })
  }
 
  const user = await createUser(result.data)
  return res.status(201).json(user)
})

Dla frontendowca najważniejsze jest to, żeby backend zwracał błędy w stabilnym formacie. Wtedy UI nie musi parsować losowych stringów i może mapować errors.email na konkretne pole formularza.

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. Listy produkcyjne powinny mieć paginację od początku. 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 nieprzezroczysty token (często base64) oparty na id, created_at albo innym stabilnym kluczu 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, czyli bezpieczne retry POST

Frontend często musi ponowić request. Ma to miejsce, kiedy użytkownik ma słaby internet, serwer odpowiada za wolno albo po drodze pojawia się timeout. Przy GET zwykle nie ma problemu, ale 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 i DELETE są idempotentne z definicji HTTP, o ile backend implementuje je zgodnie z semantyką metody. POST nie jest idempotentny z definicji, a PATCH zależy od konkretnej operacji — set status=paid może być idempotentne, ale increment quantity by 1 już nie.

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

OpenAPI i kontrakt API

Najgorszy rodzaj integracji to taki, w którym frontend zgaduje kształt odpowiedzi na podstawie jednego przykładu ze Slacka. Kontrakt API powinien być jawny: endpointy, parametry, statusy, request body, response body i przykłady błędów.

W REST najczęściej robi się to przez OpenAPI:

Code
paths:
  /api/users:
    post:
      summary: Create user
      requestBody:
        required: true
      responses:
        '201':
          description: User created
        '422':
          description: Validation failed

Co to daje frontendowi:

  • mniej zgadywania przy integracji,
  • możliwość wygenerowania typów TypeScript,
  • jasne statusy i formaty błędów,
  • prostsze mockowanie API w testach,
  • łatwiejsze wykrywanie breaking changes.

Jeśli pracujesz w TypeScripcie, alternatywą może być tRPC, typed route handlers albo wspólne schematy Zod/Valibot. Ważne nie jest narzędzie, tylko to, żeby kontrakt był sprawdzalny i aktualny.

BFF — Backend for Frontend

BFF, czyli Backend for Frontend, to wzorzec, w którym frontend ma „swój” backend dopasowany do potrzeb konkretnego UI. Chodzi tutaj 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 — klucze API nie wyciekają do klienta.
  • Transformacje per platformę — inne API dla web, inne dla mobile.

Minusem jest dodatkowa warstwa do utrzymania i wtedy łatwo o nadużycie (BFF puchnie do całego backendu).

Observability — logi, request ID i debugowanie

Frontendowiec często widzi tylko komunikat: „coś poszło nie tak”. Backend powinien dawać zespołowi sposób na znalezienie konkretnego requestu w logach.

Minimum produkcyjne:

  • request ID — unikalny identyfikator requestu zwracany w nagłówku, np. X-Request-ID,
  • strukturalne logi — JSON z metodą, ścieżką, statusem, czasem odpowiedzi i user ID, jeśli jest dostępny,
  • metryki — liczba requestów, p95/p99 latency, błędy 4xx/5xx,
  • monitoring błędów — np. Sentry, Datadog, OpenTelemetry,
  • bezpieczne logowanie — bez haseł, tokenów, pełnych danych kart i wrażliwych danych osobowych.

Przykładowa odpowiedź może zawierać request ID:

Code
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
X-Request-ID: req_01JABC123
Code
{
  "type": "https://api.example.com/errors/internal",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Unexpected error",
  "requestId": "req_01JABC123"
}

Dzięki temu użytkownik może wysłać supportowi identyfikator błędu, frontend może dołączyć go do raportu, a backend znajduje dokładny wpis w logach.

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. Przeglądarka domyślnie blokuje odczyt odpowiedzi między różnymi originami (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, niestandardowe nagłówki, 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.
  • Subdomena ma znaczenie — app.example.com i api.example.com to różne originy.
  • Port ma znaczenie — localhost:3000 ≠ localhost:5173.

CORS to nie autoryzacja

CORS kontroluje, czy JavaScript w przeglądarce może odczytać odpowiedź z innego origina. 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ą.

Nie myl też CORS z ochroną przed CSRF. Jeśli używasz cookies i sesji, nadal potrzebujesz poprawnego SameSite, tokenów CSRF albo innej strategii ochrony operacji mutujących.

Checklist przed integracją endpointu

Zanim frontend zacznie podpinać endpoint, najlepiej ustalić kilka rzeczy:

  1. Jaki jest pełny URL, metoda HTTP i wymagane nagłówki?
  2. Czy endpoint wymaga sesji, tokenu, ról albo konkretnych uprawnień?
  3. Jak wygląda request body i które pola są opcjonalne?
  4. Jakie statusy może zwrócić endpoint: 200, 201, 204, 401, 403, 409, 422, 429, 5xx?
  5. Jaki jest stabilny format błędu i czy błędy formularza są mapowane per pole?
  6. Czy lista ma paginację, sortowanie, filtrowanie i limit maksymalny?
  7. Czy operację można retry'ować i czy wymaga idempotency key?
  8. Czy odpowiedź można buforować w cache'u i jak długo?
  9. Czy endpoint zwraca request ID do debugowania?
  10. Czy istnieje OpenAPI, typ TypeScript, schemat Zod albo inny kontrakt?

Jeśli nie znasz odpowiedzi na te pytania, frontend i tak będzie musiał je odkryć metodą prób i błędów. Lepiej ustalić kontrakt, zanim UI zacznie zależeć od domysłów.

Problem w UI → możliwa przyczyna backendowa

Objaw w interfejsieMożliwa przyczyna backendowa
Lista długo pokazuje spinnerBrak indeksu, N+1, brak paginacji, wolne JOIN-y
Formularz pokazuje ogólny błądBrak strukturalnych błędów per pole
Użytkownik jest wyrzucany do logowania401, wygasła sesja, problem z cookie/tokenem
Zalogowany użytkownik widzi "brak dostępu"403, brak roli lub uprawnienia
Podwójne zamówienie po kliknięciuBrak idempotency key albo blokady po stronie UI
Request działa w Postmanie, ale nie w UICORS, cookies, credentials, różny origin
Infinite scroll gubi lub duplikuje daneOffset pagination przy zmieniającym się zbiorze
Produkcja działa wolniej niż stagingBrak poolingu, zimne starty, inna skala danych

Werdykt Labu

Serwer, baza danych i mają fundamentalne znaczenie dla aplikacji webowej. Frontendowiec, który rozumie statusy HTTP, strukturę błędów, paginację i , 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, tylko 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.

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 — tam, gdzie żyją dane3 min
  • 3. API — umowa między frontem a backendem6 min
  • 4. CORS — dlaczego "from origin" wkurza frontendowca2 min
  • Checklist przed integracją endpointu1 min
  • Problem w UI → możliwa przyczyna backendowa1 min
  • Werdykt Labu1 min
  • Pozostałe części serii1 min

Często zadawane pytania

Źródła i dokumentacjaZweryfikowano: 11 czerwca 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, kolejki, deployment, monitoring, OWASP Top 10:2025 i RODO — backendowa wiedza, którą frontendowiec powinien znać.

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
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, integracje zewnętrzne, sesje, JWT, cookies, CSRF, refresh token rotation i MFA.

Maciej Sala

Maciej Sala

Founder Strivelab

29 lipca 2025

Poprzedni wpis

Next.js a SEO — kiedy naprawdę daje przewagę nad zwykłym Reactem
Next.js a SEO — kiedy naprawdę daje przewagę nad zwykłym Reactem

Next.js naprawdę poprawia SEO. 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 wpis

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, integracje zewnętrzne, sesje, JWT, cookies, CSRF, refresh token rotation i MFA.

Maciej Sala

Maciej Sala

Founder Strivelab

29 lipca 2025