Przejdź do treści

Backend dla frontendowca: Cache, deployment, security

Redis, cache HTTP, kolejki, monitoring i bezpieczeństwo (OWASP Top 10:2025, RODO). Poznaj backendową wiedzę, którą musi znać każdy frontendowiec.

Maciej Sala

Founder StriveLab

18 min czytaniaOpublikowano 30 lipca 2025 (Aktualizacja 14 lipca 2026)

Jeśli projekt ma działać poza lokalnym środowiskiem to nie są dodatki „na później”. Cache potrafi zdjąć ogromną część ruchu z bazy, ale może też zwracać nieaktualne dane. Kolejka ratuje przy wolnych operacjach, ale wymaga idempotencji.

1. Cache w backendzie i przyspieszanie odpowiedzi

Cache nie jest tylko optymalizacją na koniec projektu. To sposób na zdjęcie pracy z bazy, API zewnętrznych i serwera, ale też źródło trudnych błędów, jeśli nie wiesz, kiedy dane stają się nieaktualne.

Redis

Redis to in-memory database często używana jako cache, magazyn sesji, kolejka albo mechanizm pub/sub. Warto też znać Valkey, czyli forka Redis wspieranego przez Linux Foundation po zmianach licencyjnych w ekosystemie Redis. W najprostszym wariancie trzymasz w nim wynik wolnego zapytania przez kilka minut:

Code
async function getUsers() {
  // Sprawdź cache
  const cached = await redis.get('users:all')
  if (cached) return JSON.parse(cached)
 
  // Pobierz z bazy
  const users = await db.query('SELECT * FROM users')
 
  // Zapisz w cache na 5 minut
  await redis.set('users:all', JSON.stringify(users), 'EX', 300)
 
  return users
}

W praktyce równie ważna jak samo zapisanie jest strategia unieważniania cache po zmianie danych.

Kiedy cachować odpowiedzi backendu?

Cache ma sens tam, gdzie odczyt jest częsty, a zmiana rzadsza albo akceptujesz chwilowo nieświeże dane. Nie cachuj wszystkiego automatycznie, bo każdy cache trzeba potem unieważnić.

  • Dane często czytane, rzadko zmieniane
  • Wolne zapytania do bazy
  • Wywołania zewnętrznych API
  • Obliczenia / agregacje

Strategie cache w aplikacji webowej

Cache-aside () — najczęstsze:

Code
1. Sprawdź cache
2. Brak? → pobierz z bazy → zapisz do cache → zwróć

Write-through — zapis idzie najpierw do cache, potem do bazy. Cache zawsze świeży.

Write-behind — zapis do cache od razu, do bazy asynchronicznie. Najszybsze, ale ryzyko utraty danych.

Stale-while-revalidate — zwróć stary cache od razu, w tle odśwież.

Unieważnianie cache w backendzie

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

Problem polega na tym, że cache przyspiesza odczyt, ale oddala Cię od źródła prawdy. Masz trzy podstawowe podejścia:

  1. TTL (expire after N) — proste, przewidywalne, ale dane mogą być nieaktualne przez chwilę.
  2. Event-driven — po UPDATE w bazie wyczyść konkretne klucze (del('user:123')).
  3. Versioning — klucz zawiera wersję (user:123:v5); zmieniasz wersję, stare wpisy zostają, ale nikt ich nie czyta.
Code
// Event-driven invalidation
async function updateUser(id, data) {
  await db.user.update({ where: { id }, data })
  await redis.del(`user:${id}`) // pojedynczy klucz
 
  // Redis DEL nie usuwa po patternie. Klucze list warto zapisywać pod tagiem.
  const listKeys = await redis.smembers('tag:users:list')
  if (listKeys.length > 0) await redis.unlink(...listKeys)
  await redis.del('tag:users:list')
}

Nie używaj KEYS users:list:* na produkcji, bo może zablokować Redis przy dużej liczbie kluczy. Jeśli musisz usuwać po wzorcu, użyj SCAN partiami albo trzymaj własny indeks kluczy per tag, jak na powyższym przykładzie.

Cache stampede i ochrona przed przeciążeniem

Cache może też zaszkodzić, gdy popularny klucz wygasa i nagle setki requestów jednocześnie idą do bazy. To cache stampede. Typowe zabezpieczenia:

  • jitter w TTL — dodaj losowe kilka–kilkadziesiąt sekund, żeby klucze nie wygasały naraz

  • lock per klucz — tylko jeden request odbudowuje cache, reszta czeka albo dostaje stary wynik

  • stale-while-revalidate — użytkownik dostaje starszą odpowiedź, a backend odświeża cache w tle

  • pre-warming — najważniejsze klucze odświeżasz zanim wygasną.

HTTP cache jako pierwszy poziom cache

Backend Redis to nie wszystko, ponieważ przeglądarka i cache'ują na bazie nagłówków HTTP. Frontendowiec widzi to w Network tabie jako (disk cache) / (memory cache) / from CDN.

Code
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
ETag: "abc123"
Last-Modified: Wed, 15 Apr 2026 10:00:00 GMT
HeaderCo robi
Cache-Control: publicCDN może cachować
Cache-Control: privateTylko przeglądarka użytkownika
Cache-Control: no-storeNie cachuj wcale
Cache-Control: no-cacheCachuj, ale zawsze rewaliduj
max-age=3600Cache świeży przez godzinę
s-maxage=3600TTL tylko dla CDN (override max-age)
stale-while-revalidate=NPo expiry serwuj stary, w tle pobierz świeży
stale-if-error=NPo błędzie serwuj stary (graceful degradation)
ETag: "abc"Hash zawartości — przeglądarka pyta "czy jest nowsze?"

Conditional request:

Code
GET /api/posts/1
If-None-Match: "abc123"

→ 304 Not Modified  (zero bajtów body!)

Trzy poziomy cache w aplikacji webowej

Code
Browser cache  →  CDN (Cloudflare, Fastly)  →  Redis (backend)  →  Database
   ~0ms              ~30ms                       ~2ms               100ms+

Czasy są poglądowe i mierzone z różnych miejsc: cache przeglądarki nie wymaga sieci w ogóle, CDN liczy się od użytkownika, a Redis i baza — od serwera aplikacji (z perspektywy użytkownika dochodzi do nich jeszcze droga sieciowa do backendu). Dobra aplikacja cachuje na każdym poziomie, gdzie ma to sens.

Cache frameworka i cache Next.js

Nowoczesne frameworki mają własne warstwy:

  1. Next.js Data Cache. Cachuje wyniki fetch() z revalidate.
  2. Next.js Full Route Cache. Całe HTML strony statycznej.
  3. React cache(). Deduplikacja w obrębie jednego renderu.
  4. use cache. Aktualny model cache w Next.js 16 przy włączonych Cache Components.
  5. unstable_cache. Starsze API z tagami i rewalidacją, nadal spotykane w projektach sprzed migracji.
Code
import { cacheTag, revalidateTag } from 'next/cache'
 
export async function getPosts() {
  'use cache'
  cacheTag('posts')
  return db.post.findMany()
}
 
// Po update:
revalidateTag('posts')

W Next.js 16 unstable_cache zostało zastąpione przez dyrektywę use cache. Ważna zasada pozostaje taka sama: nie czytaj cookies() ani headers() bezpośrednio we współdzielonym zakresie cache. Dane zależne od użytkownika przekaż jako argument albo użyj mechanizmu przeznaczonego do prywatnego cache.

2. Kolejki i zadania w tle

Nie każdy proces powinien kończyć się w czasie jednego requestu. Wysyłka maila, przeliczenie raportu, generowanie PDF-a, synchronizacja CRM czy przetwarzanie płatności mogą trwać za długo albo zależeć od zewnętrznego systemu. Kolejka pozwala szybko odpowiedzieć użytkownikowi, a ciężką pracę wykonać w tle.

Code
// Zamiast:
app.post('/api/send-email', async (req, res) => {
  await sendEmail(req.body) // 2-3 sekundy!
  res.json({ success: true })
})
 
// Lepiej:
app.post('/api/send-email', async (req, res) => {
  await queue.add('send-email', req.body) // Natychmiast
  res.json({ queued: true })
})
 
// Worker przetwarza w tle
queue.process('send-email', async (job) => {
  await sendEmail(job.data)
})

Popularne: BullMQ (Redis), RabbitMQ, AWS SQS, Inngest, Trigger.dev (event-driven, type-safe).

Idempotencja i deduplikacja zadań w kolejce

Kolejka nie gwarantuje automatycznie, że zadanie zostanie wykonane dokładnie jeden raz. W rzeczywistości wiele systemów działa w modelu at-least-once, co oznacza, że operacja może zostać powtórzona – na przykład po ponownej próbie, restarcie procesu lub chwilowej awarii. Z tego powodu zadania muszą być idempotentne.

Code
await queue.add(
  'send-email',
  { userId, template: 'welcome' },
  { jobId: `welcome-email:${userId}` },
)

Po stronie workera nadal warto zapisać efekt w bazie:

Code
async function processWelcomeEmail(job) {
  const alreadySent = await db.emailLog.findUnique({
    where: { key: `welcome-email:${job.data.userId}` },
  })
 
  if (alreadySent) return
 
  await sendEmail(job.data)
  await db.emailLog.create({
    data: { key: `welcome-email:${job.data.userId}` },
  })
}

Reguła: retry ma naprawiać błędy przejściowe, a nie tworzyć duplikaty.

Cron i zadania cykliczne

Cron to praca cykliczna, czyli raport nocny, czyszczenie cache, naliczenie subskrypcji, przypomnienia mailowe. Prosty mechanizm, ale w produkcji musi być idempotentny i monitorowany, ponieważ dwa równoległe uruchomienia potrafią narobić szkód.

Code
// node-cron
import cron from 'node-cron'
 
cron.schedule('0 2 * * *', async () => {
  // codziennie o 2:00
  await sendDailyDigest()
})

Reguły dla cron:

  • Idempotency — zadanie może odpalić się dwa razy (deploy, retry)
  • Lock — przy wielu instancjach tylko jedna ma wykonać (Redis lock, advisory lock w Postgres)
  • Monitoring — alert jeśli zadanie się nie wykonało (Healthchecks.io, Better Stack)

W produkcji użyj zarządzanej platformy: Vercel Cron, Cloudflare Cron Triggers, Inngest, GitHub Actions (cron + curl), Kubernetes CronJob.

Wzorce przetwarzania zadań w tle

  • Fan-out: jedno zdarzenie inicjuje wiele niezależnych zadań (np. nowy użytkownik wyzwala wysyłkę e-maila, tworzenie przestrzeni roboczej i zapis w CRM).

  • Pipeline: zadania są realizowane sekwencyjnie, gdzie ukończenie zadania A uruchamia zadanie B, a następnie C.

  • Ponawianie z wykładniczym opóźnieniem(retry with backoff): w razie błędu system podejmuje ponowne próby wykonania zadania, stopniowo wydłużając czas oczekiwania między nimi (np. 1s, 2s, 4s, 8s...).

  • Kolejka martwych wiadomości (Dead Letter Queue - DLQ): zadania, które wielokrotnie zakończyły się niepowodzeniem, są przenoszone do specjalnej kolejki w celu manualnej analizy.

Warto również ustawić alert, w sytuacji gdy kolejka martwych wiadomości rośnie, system może nadal odpowiadać kodem 200, jednak z biznesowego punktu widzenia przestaje efektywnie wykonywać swoją pracę.

3. File storage i upload plików

Pliki mają inne wymagania niż rekordy w bazie. Obrazy, dokumenty, avatary i eksportowane PDF-y zwykle nie powinny trafiać bezpośrednio do relacyjnej bazy danych. Najczęściej zapisujesz je w object storage, a w bazie trzymasz tylko metadane i ścieżkę.

Code
const key = `uploads/${filename}`
 
await storage.putObject({
  bucket: 'my-bucket',
  key,
  body: fileBuffer,
  contentType: mimeType,
})
 
// Zapisz URL w bazie
await db.user.update({
  where: { id: userId },
  data: { avatar: `https://cdn.example.com/${key}` },
})

Popularne: AWS S3, Cloudflare R2 (zero egress fee!), Google Cloud Storage, Backblaze B2, MinIO (hostowane samodzielnie).

Presigned URLs i upload bezpośrednio z frontendu

W większych systemach backend nie powinien przepuszczać każdego pliku przez siebie. Zamiast tego generuje presigned URL, a frontend uploaduje plik bezpośrednio do storage. Backend kontroluje uprawnienia i zapisuje metadane, ale nie musi trzymać całego pliku w pamięci.

Code
// Backend — generuje presigned URL
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { PutObjectCommand } from '@aws-sdk/client-s3'
 
app.post('/api/upload-url', async (req, res) => {
  const userId = req.user.id
  const filename = req.body.filename.replace(/[^a-z0-9._-]/gi, '-')
  const key = `uploads/${userId}/${crypto.randomUUID()}-${filename}`
 
  const url = await getSignedUrl(
    s3,
    new PutObjectCommand({ Bucket, Key: key, ContentType: req.body.type }),
    { expiresIn: 300 }, // 5 minut
  )
 
  res.json({ url, key })
})
 
// Frontend — upload bezpośrednio do S3
const { url, key } = await fetch('https://api.example.com/upload-url', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ filename: file.name, type: file.type }),
}).then((res) => res.json())
 
await fetch(url, {
  method: 'PUT',
  body: file,
  headers: { 'Content-Type': file.type },
})
 
await fetch('https://api.example.com/save-meta', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key }),
})

Plusy:

  • Backend nie przepuszcza pliku przez siebie — oszczędza pamięć i transfer
  • Skalowalne — S3 wytrzyma więcej niż Twój serwer
  • Szybsze dla użytkownika — brak przejścia przez backend

Bezpieczeństwo uploadu plików

Presigned URL nie znaczy „dowolny upload”. Backend nadal musi narzucić zasady:

  • allowlista MIME — np. tylko image/jpeg, image/png, image/webp, application/pdf

  • limit rozmiaru — najlepiej egzekwowany polityką uploadu albo po stronie storage

  • bezpieczny klucz — nie ufaj nazwie pliku od użytkownika; generuj własny prefiks i UUID

  • prywatny bucket domyślnie — publiczny dostęp dawaj tylko tam, gdzie naprawdę jest potrzebny

  • walidacja po uploadzie — sprawdź metadata, rozmiar i typ zanim zapiszesz plik jako aktywny

  • skanowanie plików — szczególnie przy PDF-ach, dokumentach i uploadach dostępnych dla innych użytkowników

  • osobny etap publikacji — upload może trafić do katalogu tymczasowego, a dopiero worker przenosi go do docelowej lokalizacji.

CDN dla statycznych plików

Pliki publiczne (avatary, obrazki postów) idą zwykle przez CDN — Cloudflare, Bunny, CloudFront:

Code
Origin: https://my-bucket.s3.amazonaws.com/uploads/avatar.jpg
CDN:    https://cdn.example.com/uploads/avatar.jpg  (bliżej użytkownika, cache'owane)

Optymalizacja obrazów po uploadzie

Surowe obrazy są ciężkie i tutaj mamy różne metody optymalizacji. Najpopularniejsze podejścia:

  1. Next.js Image — automatyczne resize, WebP / AVIF, lazy loading.
  2. Cloudinary / imgix / Cloudflare Images — managed image CDN z transformacjami w URL.
  3. Sharp (Node) — backend-side resize / optimize przy upload.

Na blogu StriveLab wykorzystuje Next.js Image oraz Sharp.

4. Środowiska i deployment backendu

Kod działający lokalnie to dopiero początek zabawy, ponieważ backend musi mieć środowiska, sekrety, migracje, monitoring, backupy i powtarzalny deployment.

Środowiska aplikacji: development, staging i produkcja

Code
Development (localhost)
    ↓
Staging (testowanie)
    ↓
Production (użytkownicy)

Zmienne środowiskowe i sekrety

Code
# .env
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=super-secret-key
REDIS_URL=redis://localhost:6379
Code
const dbUrl = process.env.DATABASE_URL

Nigdy nie commituj .env do repo!

Opcje deploymentu dla backendu

Nie ma jednego najlepszego hostingu dla backendu i dlatego, wybór zależy od tego, czy chcesz prostoty, kontroli, globalnego edge, czy przewidywalnych kosztów.

Platform as a Service (łatwe):

  • Vercel (Next.js)
  • Railway
  • Render
  • Heroku

Container (więcej kontroli):

  • Docker + Kubernetes
  • AWS ECS
  • Google Cloud Run

VPS (pełna kontrola):

  • DigitalOcean
  • AWS EC2
  • Hetzner (Niemcy, świetna cena/wydajność)
  • Coolify (PaaS hostowany samodzielnie na własnym VPS)

Edge / :

  • Cloudflare Workers
  • Vercel Functions (Fluid compute)
  • Deno Deploy
  • AWS Lambda + API Gateway
  • Fly.io (globalne VM)

CI/CD i automatyzacja deploymentu

Pipeline w GitHub Actions albo GitLab CI sprawia, że testy, build i wdrożenie są powtarzalne:

Code
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
      - run: npm run lint
 
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx vercel@latest deploy --prod --token=${{ secrets.VERCEL_TOKEN }}

Minimum sensownego pipeline'u:

  1. Instalacja zależności z lockfile (npm ci, pnpm install --frozen-lockfile).
  2. Lint, testy jednostkowe i testy typów.
  3. Build produkcyjny.
  4. Skan sekretów i zależności.
  5. Migracje bazy w kontrolowanym kroku.
  6. Deploy.
  7. Smoke test po wdrożeniu.

Strategie deploymentu bezpieczne dla produkcji

  • Blue-green. Dwa środowiska, switch ruchu w jednej chwili (zero downtime)
  • Canary. Nowa wersja dostaje 1% ruchu, jeśli nie pada → 10% → 50% → 100%
  • Rolling. Instancje aktualizowane po kolei (Kubernetes default)
  • Feature flags. Kod wdrożony, ale wyłączony; włączany per użytkownik albo kohorta (LaunchDarkly, Unleash, GrowthBook)

Migracje bazy danych bez downtime

Najczęstszy błąd przy deploymentach backendu to założenie, że kod i baza zmienią się dokładnie w tej samej sekundzie. W realnym wdrożeniu przez chwilę mogą działać dwie wersje kodu: stara i nowa. Migracje muszą być z nimi kompatybilne.

Bezpieczny wzorzec to expand → migrate → contract:

  1. Expand. Dodaj nową kolumnę/tabelę jako opcjonalną, bez usuwania starego pola.
  2. Deploy kodu. Nowa wersja potrafi pisać do starego i nowego modelu albo czytać oba.
  3. Backfill. Uzupełnij dane w tle, partiami.
  4. Przełącz odczyt. Aplikacja zaczyna czytać z nowej struktury.
  5. Contract. Dopiero po czasie usuń stare pole.

Unikaj migracji, które długo blokują tabele w godzinach ruchu. Przy dużych tabelach indeksy, backfill i zmiany NOT NULL trzeba planować osobno.

Health checks, readiness i rollback

Backend powinien mieć przynajmniej dwa endpointy kontrolne:

Code
GET /healthz  → proces żyje
GET /readyz   → proces może obsługiwać ruch (baza, Redis, migracje OK)

Po wdrożeniu uruchom smoke test: zalogowanie testowego użytkownika, podstawowy endpoint API, zapis/odczyt z bazy i jeden krytyczny flow biznesowy. Rollback też musi być przećwiczony: jeśli nowy kod jest cofany, baza nadal musi być kompatybilna z poprzednią wersją aplikacji.

Backups i disaster recovery

Backup jest użyteczny dopiero wtedy, gdy potrafisz go odtworzyć. Sama informacja „mamy backupy” niewiele znaczy, jeśli nikt nigdy nie sprawdził restore'u.

  • Automatyczne backupy bazy (PostgreSQL: pg_dump, point-in-time recovery)
  • Test restore — backup, którego nie odtworzyłeś, nie istnieje
  • RTO (Recovery Time Objective) — jak długo system może być niedostępny
  • RPO (Recovery Point Objective) — ile danych możesz stracić

5. Monitoring i logging backendu

Bez monitoringu backend jest czarną skrzynką. Widzisz tylko, że użytkownik zgłasza problem, ale nie wiesz, czy zawiodła baza, zewnętrzne API, deployment, cache czy konkretna ścieżka w kodzie.

Logi aplikacji backendowej

Code
// Prosty logging
console.log('User created:', userId)
 
// Strukturalny logging
logger.info('User created', {
  userId,
  email,
  timestamp: new Date(),
})

Narzędzia: Winston, Pino, Datadog, Papertrail

Monitoring backendu

Monitoring odpowiada na pytanie, czy system działa zdrowo. Logi mówią, co się wydarzyło w konkretnym przypadku, a metryki pokazują trend i skalę problemu.

  • Uptime — czy serwer działa?
  • Latency — jak szybko odpowiada?
  • Errors — ile błędów 5xx?
  • Resources — CPU, RAM, disk

Narzędzia: Grafana, Datadog, New Relic, Sentry (errors), Better Stack, Honeycomb.

Trzy filary observability

  1. Logs — co się stało (event-by-event)
  2. Metrics — ile czego (latency, throughput, errors)
  3. Traces — pełna ścieżka requestu przez system (request-id przez wszystkie usługi)

Minimum praktyczne dla frontendowca i backendowca to wspólny request ID. Backend zwraca go w nagłówku, frontend dołącza do raportu błędu, a logi pozwalają znaleźć dokładny request.

Code
X-Request-ID: req_01JABC123

Distributed tracing w aplikacji webowej

W większych systemach pojedynczy request często przechodzi przez kilka usług. Bez trace'u widzisz tylko „endpoint trwa 800 ms”. Z trace'em widzisz, że 600 ms zjada konkretne zapytanie do bazy albo jedna integracja zewnętrzna.

Code
[trace-id: abc] frontend → API gateway → user service → auth service → database
                  120ms    8ms           45ms           12ms          50ms

Standard: OpenTelemetry (otel) — niezależny od dostawcy; eksportuje do Datadog, Honeycomb, Tempo, Jaeger.

Alerty produkcyjne: co powinno budzić w nocy?

Alert powinien odpowiadać na objaw, nie na przyczynę. SRE Google definiuje tzw. „Four Golden Signals":

  • Latency — p50 / p95 / p99 czasu odpowiedzi
  • Traffic — RPS (requests per second)
  • Errors — % błędów 5xx
  • Saturation — wykorzystanie CPU, RAM, disku, pool connections

Alert niech budzi tylko wtedy, gdy użytkownik faktycznie odczuwa problem. Reszta to ticket „do obejrzenia”.

Observability od frontendu do backendu

Frontend też powinien wysyłać błędy i metryki. Sentry, LogRocket, Datadog RUM pokazują, jak użytkownik dochodzi do błędu. Powiąż user-id / request-id między frontem a backendem, żeby widzieć całą ścieżkę.

6. Bezpieczeństwo backendu dla frontendowca

Bezpieczeństwo backendu to zestaw odpowiednik nawyków (w uproszczeniu): walidujesz dane, ograniczasz uprawnienia, nie ufasz klientowi, logujesz zdarzenia i zakładasz, że każdy publiczny endpoint będzie testowany przez kogoś nieżyczliwego.

Podstawowe zasady bezpieczeństwa backendu

  1. Waliduj input. Nigdy nie ufaj danym od użytkownika
  2. Parametryzuj zapytania. Unikaj injection.
  3. Hashuj hasła. bcrypt, argon2.
  4. HTTPS everywhere. Szyfruj transmisję.
  5. Rate limiting. Ogranicz requesty.
  6. Autoryzuj na backendzie. może ukryć przycisk, ale nie może być źródłem prawdy.
  7. CORS. Karltonontroluj, które originy mogą czytać odpowiedzi z API.
Code
// ❌ SQL Injection
db.query(`SELECT * FROM users WHERE id = ${userId}`)
 
// ✅ Parametryzowane zapytanie
db.query('SELECT * FROM users WHERE id = $1', [userId])
Code
// ❌ Przechowywanie hasła
user.password = 'password123'
 
// ✅ Hashowanie
user.passwordHash = await bcrypt.hash('password123', 10)

Rate limiting w praktyce

Code
import rateLimit from 'express-rate-limit'
 
// Globalny limit
app.use(
  '/api/',
  rateLimit({
    windowMs: 60 * 1000, // okno 1 minuta
    max: 60, // 60 requestów / minutę / IP
    standardHeaders: true, // X-RateLimit-* + Retry-After
    legacyHeaders: false,
    message: {
      type: 'about:blank',
      title: 'Too Many Requests',
      status: 429,
      detail: 'Try again in a moment',
    },
  }),
)
 
// Bardziej restrykcyjny dla loginu
app.post(
  '/api/auth/login',
  rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minut
    max: 5, // 5 prób
    skipSuccessfulRequests: true,
  }),
  loginHandler,
)

Frontend reaguje na 429:

Code
async function fetchWithRetry(url, opts, attempt = 0) {
  const res = await fetch(url, opts)
  if (res.status === 429 && attempt < 3) {
    const retryAfter = Number(res.headers.get('Retry-After')) || 2 ** attempt
    await new Promise((r) => setTimeout(r, retryAfter * 1000))
    return fetchWithRetry(url, opts, attempt + 1)
  }
  return res
}

W produkcji rate limiting zwykle siedzi na CDN / WAF (Cloudflare, AWS WAF), a nie w aplikacji. Chroni przed DDoS i nie zużywa zasobów backendu.

CORS to nie autoryzacja

CORS mówi przeglądarce, czy JavaScript z danego origina może odczytać odpowiedź. Nie blokuje curl, aplikacji mobilnej, backendu ani atakującego, który woła API bezpośrednio. Dlatego CORS jest warstwą kontroli przeglądarki, a nie mechanizmem uprawnień.

Code
app.use(
  cors({
    origin: ['https://app.example.com'],
    credentials: true,
  }),
)

Backend nadal musi sprawdzić sesję, token, role i właściciela zasobu.

OWASP Top 10 jako minimum bezpieczeństwa

OWASP Top 10 to lista najważniejszych kategorii ryzyk aplikacji webowych. Aktualną wersją jest OWASP Top 10:2025. Nie musisz znać każdego scenariusza ataku na pamięć, ale jako frontendowiec powinieneś rozumieć, które problemy są rozwiązywane wyłącznie po stronie backendu.

  1. Broken Access Control. Sprawdzanie uprawnień na backendzie, nie tylko ukrywanie przycisków w UI.
  2. Security Misconfiguration. Debug w produkcji, otwarte buckety, domyślne hasła, złe nagłówki.
  3. Software Supply Chain Failures. Zależności, build pipeline, lockfile, paczki npm, obrazy Dockera.
  4. Cryptographic Failures. HTTPS wszędzie, bezpieczne algorytmy, brak danych wrażliwych w logach.
  5. Injection. SQL injection, XSS, NoSQL injection, command injection.
  6. Insecure Design. Przykładowo, brak rate limitu na reset hasła albo brak modelu uprawnień.
  7. Authentication Failures. Słabe hasła, brak MFA, długie sesje, zły reset hasła.
  8. Software or Data Integrity Failures. Niepodpisane artefakty, niekontrolowane aktualizacje, zaufanie do niezweryfikowanych danych.
  9. Security Logging and Alerting Failures. Nie wiesz, że trwa atak albo wyciek.
  10. Mishandling of Exceptional Conditions. Wyjątki ujawniają dane, pomijają autoryzację albo zostawiają system w złym stanie.

SSRF nie zniknął jako problem, ponieważ w OWASP Top 10:2025 został włączony w szerszą kategorię Broken Access Control.

XSS, czyli Cross-Site Scripting

Atakujący wstrzykuje JS w kontekst Twojej domeny (komentarz, profil). Skradzione cookies, wyświetlone fałszywe formularze.

Obrona:

  • Escapuj output. React i większość frameworków robi to domyślnie. Uważaj na dangerouslySetInnerHTML.
  • Content-Security-Policy (CSP). Header, który mówi przeglądarce, skąd wolno ładować skrypty:
    Code
    Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com
    
  • Sanityzuj HTML od użytkownika — DOMPurify, sanitize-html

Walidacja inputu po stronie backendu

Walidacja w formularzu poprawia UX, walidacja na serwerze chroni system. Użytkownik może wyłączyć JavaScript, zmodyfikować payload w DevTools albo wysłać request z własnego skryptu.

Code
import { z } from 'zod'
 
const CreateUserSchema = z.object({
  email: z.email().max(255), // Zod 4; na Zod 3 było z.string().email()
  age: z.number().int().min(13).max(120),
  role: z.enum(['user', 'admin']),
})
 
app.post('/api/users', (req, res) => {
  const result = CreateUserSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(422).json({
      type: 'about:blank',
      title: 'Validation failed',
      status: 422,
      errors: result.error.flatten().fieldErrors,
    })
  }
  // result.data jest typowane i zwalidowane
})

Klient waliduje dla UX, a serwer waliduje dla bezpieczeństwa.

Mass assignment

Code
// ❌ Niebezpieczne — użytkownik może wysłać { role: 'admin' } w body
await db.user.update({ where: { id }, data: req.body })
 
// ✅ Tylko allowlistowane pola
const { name, bio } = req.body
await db.user.update({ where: { id }, data: { name, bio } })

Secret management w backendzie

Sekrety nigdy nie idą do repo, nawet do prywatnego.

  • Lokalnie: .env + .gitignore, direnv, 1Password CLI
  • CI/CD: GitHub Secrets, GitLab CI Variables
  • Produkcja: AWS Secrets Manager, HashiCorp Vault, Doppler, Infisical
  • Rotacja — sekrety powinny mieć datę ważności (klucze API, hasła do bazy)

Jeśli sekret kiedyś trafił do repo (nawet usuniętego commita) — uznaj go za skompromitowany i wymień. Git history żyje wiecznie.

Supply chain i bezpieczeństwo zależności

Bezpieczeństwo zależy też od tego, co instalujesz i jak budujesz aplikację:

  • commituj lockfile i instaluj zależności komendą deterministyczną (npm ci, pnpm --frozen-lockfile),
  • włącz Dependabot albo Renovate,
  • używaj npm audit z rozsądkiem: nie każda podatność dotyczy ścieżki produkcyjnej, ale krytycznych nie ignoruj,
  • pinuj obrazy Dockera do wersji albo digestu,
  • skanuj sekrety w CI,
  • ogranicz uprawnienia tokenów CI/CD do minimum,
  • nie uruchamiaj skryptów z internetu bez sprawdzenia, co robią.

SSRF przy URL-ach od użytkownika

SSRF pojawia się, gdy backend pobiera URL podany przez użytkownika, czyli może chodzić o import obrazka, webhook testowy, parser Open Graph, PDF generator. Atakujący może próbować zmusić serwer do połączenia z adresem wewnętrznym, np. metadata service w chmurze.

Obrona:

  • allowlista hostów albo domen,
  • blokada prywatnych adresów IP (127.0.0.1, 10.0.0.0/8, 169.254.169.254, IPv6 local),
  • limit rozmiaru odpowiedzi i timeout,
  • brak przekazywania sekretów do pobieranego URL,
  • osobny worker/sandbox do pobierania nieufnych zasobów.

Security headers w aplikacji webowej

Standardowe headery, które dodaje helmet:

Code
import helmet from 'helmet'
app.use(helmet())

Zwraca między innymi:

  • Strict-Transport-Security — wymuszaj HTTPS przez N dni (HSTS)
  • X-Content-Type-Options: nosniff — przeglądarka nie zgaduje MIME
  • X-Frame-Options: DENY — chroni przed clickjacking
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy — wyłącza geolokalizację, kamerę itd. domyślnie

Sprawdź swoje headery na securityheaders.com.

GDPR i RODO: minimalne minimum

Jeśli zbierasz dane od użytkowników z UE:

  • Cel przetwarzania — wiedz, po co masz dane
  • Right to access / erasure — endpoint pozwalający eksportować i kasować dane
  • Pseudonimizacja — adres IP w logach maskuj
  • Consent — banner cookie dla narzędzi analitycznych
  • DPA — umowa powierzenia z każdym dostawcą przetwarzającym dane

Co dalej: ścieżka nauki backendu dla frontendowca

Nie próbuj uczyć się backendu przez czytanie wszystkiego naraz. Najszybciej rośnie się przez mały projekt, który dotyka kilku prawdziwych problemów: logowania, bazy, walidacji, uploadu, webhooka i deploymentu. Poniższa ścieżka układa tematy od fundamentów do architektury.

Poziom 1: fundamenty backendu

  1. HTTP — request / response, kody statusu, nagłówki
  2. Node.js + Express (lub Hono / Fastify)
  3. SQL podstawy — CRUD, JOIN, indeksy, transakcje
  4. design — zasoby, metody, statusy HTTP
  5. CORS — co to, kiedy psuje requesty, jak konfigurować

Poziom 2: praktyka integracji

  1. PostgreSQL + Prisma / Drizzle
  2. Autentykacja — sesje vs JWT, cookies, CSRF
  3. Walidacja inputu (Zod) i strukturalne błędy (RFC 9457)
  4. Deployment — Vercel / Railway / Render
  5. Webhooks — przyjmowanie i wysyłanie

Poziom 3: zaawansowane tematy backendowe

  1. Redis / Valkey — strategie cache, pub/sub, kolejki (BullMQ)
  2. Real-time — i WebSockets
  3. Docker + pipeline CI/CD
  4. Monitoring / logging / tracing (OpenTelemetry, Sentry)
  5. Connection pooling / serverless drivery (PgBouncer, Neon)

Poziom 4: architektura systemów

  1. Microservices vs monolith
  2. Event-driven architecture, kolejki
  3. CQRS, Event Sourcing
  4. Distributed systems — CAP theorem, eventual consistency
  5. Testy obciążeniowe (k6, Artillery)

Projekt do nauki backendu

Najlepszy projekt do nauki nie musi być oryginalny. Ma zmusić Cię do przejścia przez typowe decyzje backendowe. Dobrym kandydatem jest prosty blog z autentykacją:

  • Rejestracja / logowanie (sesja albo JWT),
  • postów z paginacją,
  • Komentarze (real-time z SSE — bonus),
  • Upload obrazów (presigned URLs do S3 / R2),
  • Webhook handler (Stripe sandbox jako „płatne posty”),
  • Deployment + CI/CD,
  • Sentry / monitoring.

Taki projekt nie wygląda efektownie na pierwszy rzut oka, ale pokrywa większość problemów, które wracają w komercyjnych aplikacjach: stan użytkownika, dane, uprawnienia, pliki, integracje, błędy i deployment.

KomponentRolaPopularne (2026)
SerwerLogika aplikacjiExpress, Fastify, Hono, NestJS
RuntimeWykonanie koduNode.js, Bun, Deno, Cloudflare Workers
Baza danychPrzechowywaniePostgreSQL, MySQL, MongoDB, SQLite (Turso)
ORM / query builderAbstrakcja bazyPrisma, Drizzle, Kysely
CachePrzyspieszanieRedis, Valkey, Memcached, HTTP cache
AuthBezpieczeństwoAuth.js, Clerk, Supabase Auth, Better Auth
API styleKomunikacjaREST, GraphQL, tRPC
Real-timePush z serweraSSE, WebSockets, Pusher
StoragePlikiS3, Cloudflare R2, object storage + CDN
KolejkiZadania w tleBullMQ, Inngest, Trigger.dev, SQS
WalidacjaBezpieczny inputZod, Valibot, Yup
DeploymentHostingVercel, Railway, Fly.io, Render, Cloudflare
ObservabilityCo się dziejeSentry, Datadog, Better Stack, Honeycomb
Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Wybierasz warstwę danych? Porównaj Drizzle ORM i Prisma przed wdrożeniem aplikacji.

Pozostałe części serii

Często zadawane pytania

Co to jest Redis i do czego się używa?

Redis to in-memory data store, czyli baza trzymająca dane głównie w pamięci RAM. Najczęściej używa się go jako cache, magazynu sesji, kolejki zadań albo mechanizmu pub/sub dla real-time. Redis nie zastępuje relacyjnej bazy danych — jest warstwą przyspieszającą albo pomocniczą. Warto też znać Valkey, czyli forka Redis rozwijanego po zmianach licencyjnych w ekosystemie Redis.

Co to jest connection pool i dlaczego jest ważny dla serverless?

Connection pool utrzymuje pulę gotowych połączeń do bazy, zamiast otwierać nowe połączenie przy każdym requeście. W serverless każdy request może trafić do nowej instancji, więc klasyczny pool łatwo generuje setki połączeń i przeciąża bazę. Pomagają pgbouncer, Prisma Accelerate, Neon serverless driver, Supavisor i inne poolery.

Kiedy przerzucać pracę do kolejki?

Do kolejki przerzucaj operacje, które trwają długo, mogą wymagać retry albo nie muszą blokować odpowiedzi HTTP: wysyłkę maili, generowanie PDF-ów, synchronizacje CRM, przetwarzanie webhooków i raporty. Request powinien szybko zwrócić informację, że zadanie zostało przyjęte, a worker wykonuje właściwą pracę w tle.

Czy cache powinien być w Redisie, HTTP, CDN czy frameworku?

To zależy od danych. Statyczne odpowiedzi i assety najlepiej cachować w przeglądarce i CDN przez nagłówki HTTP. Wyniki ciężkich zapytań backendowych można trzymać w Redisie albo Valkey. Frameworki, takie jak Next.js, dodają własne warstwy cache dla fetch, renderingu i rewalidacji. Najważniejsze jest nie miejsce cache, tylko strategia unieważniania.

Co backend powinien monitorować na produkcji?

Minimum to uptime, latency, błędy 5xx, wykorzystanie CPU/RAM/dysku, stan connection poola i błędy aplikacyjne. W praktyce warto mieć logi strukturalne, metryki, trace ID oraz alerty oparte na objawach odczuwanych przez użytkowników, a nie na każdej technicznej anomalii.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web 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.

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

Skontaktuj się ze mną

Biblioteka wiedzy na temat Backend

Czytaj dalej

Zobacz więcej wpisów
Strategie cache w Next.js 16: unstable_cache czy Redis?

Zarządzanie pamięcią podręczną cache w Next.js może wydawać się proste, dopóki nie napotkamy wyzwań w środowisku produkcyjnym. Wiele instancji, webhooki z systemów CMS oraz dane, które bywają nieaktualne po wdrożeniu, potrafią skomplikować sytuację. App Router oferuje rozszerzony fetch, unstable cache, deduplikację żądań oraz Cache Components. Redis z kolei dostarcza wspólny magazyn, który jest łatwiejszy do monitorowania. Zastanówmy się więc, jaki konkretny problem próbujemy rozwiązać za pomocą mechanizmów buforowania?

Maciej Sala

Maciej Sala

Founder StriveLab

Backend dla frontendowca: serwer, bazy danych i API

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 API . Jak backend paginuje dane, jak zwraca błędy, jak trzyma sesję, co robi po przekroczeniu limitu czasu i czy potrafi przyjąć większy ruch.

Maciej Sala

Maciej Sala

Founder StriveLab

Backend dla frontendowca: auth, real-time i integracje

Pierwsza część serii uporządkowała fundamenty: serwer, bazę danych, API i CORS. Teraz przechodzimy do obszarów, które zwykle pojawiają się chwilę później, gdy aplikacja przestaje być prostym CRUD-em: komunikacja w czasie rzeczywistym, webhooki, integracje zewnętrzne oraz autentykacja .

Maciej Sala

Maciej Sala

Founder StriveLab