REST API — zasady projektowania i dobre praktyki

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

Opublikowano

5 grudnia 2025 14:20

Czytanie

7 min czytania

Aktualizacja

15 kwietnia 2026 11:52

REST (Representational State Transfer) to styl architektoniczny, który mocno wpłynął na web, a w praktyce większość API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami., z którymi pracujesz jako frontend developer, to raczej HTTP API inspirowane REST niż "czysty REST" z podręcznika.

Ale REST to nie tylko "używaj HTTP". To zbiór konwencji i zasad, które sprawiają, że API jest intuicyjne, przewidywalne i łatwe w użyciu.

Krótka odpowiedź: URL to rzeczowniki (zasoby), nie czasowniki, więc GET pobiera, POST tworzy, PUT zastępuje, PATCH aktualizuje częściowo, DELETE usuwa. Idąc dalej: 200 to sukces, 201 to utworzono, 401 to brak autentykacji, 403 to brak uprawnień, 404 to brak zasobu. Bądź konsekwentny w całym API, ponieważ ostatecznie konwencje są ważniejsze niż przyjęcie puryzmu REST-owego.

W tym artykule poznasz zasady projektowania REST API to styl projektowania interfejsów oparty na zasobach, metodach HTTP i bezstanowej komunikacji. — zarówno jako konsument (frontend), jak i twórca (backend), jeśli potrzebujesz odświeżyć podstawy, zacznij od HTTP od podstaw.

Zasady REST

1. Client-Server

Klient (frontend) i serwer (backend) są oddzielone, a komunikują się przez HTTP.

2. Stateless

Każdy request powinien zawierać wszystko, co potrzebne do jego obsługi, ponieważ w realnych aplikacjach spotkasz też sesje i cookies, więc wiele API jest po prostu "REST-like", a nie całkiem idealnie stateless.

3. Cacheable

Odpowiedzi mogą być cachowane (nagłówki Cache-Control, ETag).

4. Uniform Interface

Jednolity interfejs — standardowe metody HTTP, format URL, kody statusu.

5. Layered System

Klient nie wie, czy komunikuje się z serwerem końcowym, czy pośrednikiem (load balancer, cache).

Struktura URL

Zasoby (Resources)

URL reprezentuje zasoby (rzeczowniki), a nie akcje, tak więc:

Code
✅ Dobrze (rzeczowniki):
GET /users
GET /users/123
GET /users/123/posts

❌ Źle (czasowniki):
GET /getUsers
GET /getUserById/123
POST /createUser

Kolekcje vs pojedyncze zasoby

Code
/users          → kolekcja wszystkich użytkowników
/users/123      → pojedynczy użytkownik o ID 123
/users/123/posts → posty użytkownika 123

Zagnieżdżone zasoby

Code
GET /users/123/posts         → posty użytkownika
GET /users/123/posts/456     → konkretny post użytkownika
GET /posts/456/comments      → komentarze do posta

Nie zagnieżdżaj zbyt głęboko — max 2-3 poziomy.

Konwencje nazewnictwa

Code
✅ Dobrze:
/users              (liczba mnoga)
/blog-posts         (kebab-case)
/user-profiles

❌ Źle:
/user               (liczba pojedyncza)
/blogPosts          (camelCase)
/user_profiles      (snake_case w URL)

Metody HTTP i CRUD

MetodaCRUD to skrót od Create, Read, Update, Delete, czyli podstawowych operacji wykonywanych na danych.OpisPrzykład
GETReadPobierz zasóbGET /users/123
POSTCreateUtwórz zasóbPOST /users
PUTUpdateZastąp zasóbPUT /users/123
PATCHUpdateCzęściowa aktualizacjaPATCH /users/123
DELETEDeleteUsuń zasóbDELETE /users/123

GET — pobieranie

Code
# Kolekcja
GET /users
Response: 200 OK
{
  "data": [
    { "id": 1, "name": "Jan" },
    { "id": 2, "name": "Anna" }
  ]
}
 
# Pojedynczy zasób
GET /users/1
Response: 200 OK
{ "id": 1, "name": "Jan", "email": "jan@example.com" }
 
# Nieistniejący zasób
GET /users/999
Response: 404 Not Found

POST — tworzenie

Code
POST /users
Content-Type: application/json
 
{ "name": "Piotr", "email": "piotr@example.com" }
 
Response: 201 Created
Location: /users/3
{ "id": 3, "name": "Piotr", "email": "piotr@example.com" }

PUT — pełna aktualizacja

Code
PUT /users/3
Content-Type: application/json
 
{ "name": "Piotr Nowak", "email": "piotr.nowak@example.com" }
 
Response: 200 OK
{ "id": 3, "name": "Piotr Nowak", "email": "piotr.nowak@example.com" }

PUT zazwyczaj oznacza pełną reprezentację zasobu, ale dokładne zachowanie i tak definiuje kontrakt API.

PATCH — częściowa aktualizacja

Code
PATCH /users/3
Content-Type: application/json
 
{ "email": "nowy.email@example.com" }
 
Response: 200 OK
{ "id": 3, "name": "Piotr Nowak", "email": "nowy.email@example.com" }

PATCH aktualizuje tylko przekazane pola.

DELETE — usuwanie

Code
DELETE /users/3
 
Response: 204 No Content

Kody statusu HTTP

2xx — Sukces

KodZnaczenieKiedy
200OKGET, PUT, PATCH sukces
201CreatedPOST sukces (nowy zasób)
204No ContentDELETE sukces, brak body

4xx — Błąd klienta

KodZnaczenieKiedy
400Bad RequestNieprawidłowe dane
401UnauthorizedBrak/nieprawidłowy token
403ForbiddenBrak uprawnień
404Not FoundZasób nie istnieje
409ConflictKonflikt (duplikat)
422Unprocessable EntityBłąd walidacji
429Too Many RequestsRate limit

5xx — Błąd serwera

KodZnaczenieKiedy
500Internal Server ErrorNieoczekiwany błąd
502Bad GatewayProblem z upstream
503Service UnavailableSerwer przeciążony

Format odpowiedzi

Sukces

Code
{
  "id": 123,
  "name": "Jan Kowalski",
  "email": "jan@example.com",
  "createdAt": "2025-01-15T10:30:00Z"
}

Błąd

Code
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "name", "message": "Name is required" }
    ]
  }
}

Kolekcja z metadanymi

Code
{
  "data": [
    { "id": 1, "name": "Jan" },
    { "id": 2, "name": "Anna" }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "perPage": 10,
    "totalPages": 10
  }
}

Filtrowanie, sortowanie, paginacja

Filtrowanie (query parameters)

Code
GET /users?status=active
GET /users?role=admin&status=active
GET /products?category=electronics&minPrice=100&maxPrice=500
GET /posts?author=123&createdAfter=2025-01-01

Sortowanie

Code
GET /users?sort=name           # rosnąco
GET /users?sort=-createdAt     # malejąco (minus)
GET /users?sort=role,-name     # wielokrotne

Alternatywna konwencja:

Code
GET /users?sortBy=name&order=asc

Paginacja

Offset-based:

Code
GET /users?page=2&perPage=10
GET /users?offset=10&limit=10

Cursor-based (lepsze dla dużych zbiorów):

Code
GET /users?cursor=abc123&limit=10

Offset pagination jest prostsza i wygodna np. w panelach admina. Cursor pagination lepiej zachowuje się przy dużych, stale zmieniających się zbiorach.

Odpowiedź z paginacją:

Code
{
  "data": [...],
  "meta": {
    "total": 100,
    "page": 2,
    "perPage": 10,
    "totalPages": 10,
    "nextPage": "/users?page=3&perPage=10",
    "prevPage": "/users?page=1&perPage=10"
  }
}

Wybór pól (sparse fieldsets)

Code
GET /users?fields=id,name,email
GET /users/123?fields=name,avatar

To wygodne, ale warto whitelistować pola po stronie serwera, żeby klient nie mógł wyciągać wszystkiego bez kontroli.

Include (eager loading)

Code
GET /posts/123?include=author,comments

Tak samo jak przy fields, warto posiadać limit dopuszczalnych relacji, żeby nie zrobić przypadkiem własnego "mini-GraphQL bez guardrailów".

Code
{
  "id": 123,
  "title": "Post",
  "author": { "id": 1, "name": "Jan" },
  "comments": [...]
}

Wersjonowanie API

W URL (najpopularniejsze)

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

W nagłówku

Code
GET /api/users
Accept: application/vnd.myapp.v2+json
# lub dedykowany nagłówek:
API-Version: 2

W query parameter

Code
GET /api/users?version=2

Rekomendacja: Wersjonowanie w URL jest najprostsze i najbardziej widoczne, ale nie każde API musi zaczynać od /v1, dlatego czasami wystarczy po prostu dobra kompatybilność wstecz i przemyślane polityki zmian.

Autoryzacja

Bearer Token (JWT)

Code
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

API Key

Code
GET /api/users
X-API-Key: your-api-key

Odpowiedzi

Code
# Brak tokenu
401 Unauthorized
{ "error": "Authentication required" }
 
# Token wygasł
401 Unauthorized
{ "error": "Token expired" }
 
# Brak uprawnień
403 Forbidden
{ "error": "Insufficient permissions" }

Rate Limiting

API powinno informować o limitach:

Code
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000

Po przekroczeniu:

Code
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
  "error": "Rate limit exceeded",
  "retryAfter": 60
}

Idempotency dla wrażliwych POST-ów

Tworzenie zasobów po POST nie jest idempotentne, przy płatnościach, webhookach albo retriable requestach warto dodać klucz idempotencyjny:

Code
POST /payments
Idempotency-Key: 6d2f7b2f-8f16-4f1b-a5a8-123456789abc

Serwer powinien rozpoznać ponowienie tego samego żądania i nie wykonać operacji drugi raz.

HATEOAS (opcjonalne)

Hypermedia As The Engine Of Application State, czyli linki do powiązanych zasobów:

Code
{
  "id": 123,
  "name": "Jan",
  "_links": {
    "self": { "href": "/users/123" },
    "posts": { "href": "/users/123/posts" },
    "followers": { "href": "/users/123/followers" }
  }
}

W praktyce jest to rzadko w pełni implementowane, ale warto chociaż poznać koncept.

Przykład: API do bloga

Endpoints

Code
# Posty
GET    /api/v1/posts              # lista postów
GET    /api/v1/posts/:id          # pojedynczy post
POST   /api/v1/posts              # utwórz post
PUT    /api/v1/posts/:id          # aktualizuj post
DELETE /api/v1/posts/:id          # usuń post

# Komentarze
GET    /api/v1/posts/:id/comments          # komentarze do posta
POST   /api/v1/posts/:id/comments          # dodaj komentarz
DELETE /api/v1/posts/:id/comments/:cid     # usuń komentarz

# Użytkownicy
GET    /api/v1/users/:id          # profil użytkownika
GET    /api/v1/users/:id/posts    # posty użytkownika

Przykładowe żądania

Code
# Lista postów z filtrowaniem i paginacją
GET /api/v1/posts?status=published&sort=-createdAt&page=1&perPage=10
 
# Utwórz post
POST /api/v1/posts
Authorization: Bearer <token>
Content-Type: application/json
 
{
  "title": "Mój nowy post",
  "content": "Treść posta...",
  "tags": ["javascript", "react"]
}
 
# Odpowiedź
201 Created
{
  "id": 456,
  "title": "Mój nowy post",
  "slug": "moj-nowy-post",
  "content": "Treść posta...",
  "author": {
    "id": 123,
    "name": "Jan"
  },
  "tags": ["javascript", "react"],
  "createdAt": "2025-01-15T10:30:00Z"
}

FAQ

Czym jest REST API i jak działa?

REST (Representational State Transfer) to styl architektoniczny dla API opartych na HTTP. Klient wysyła request do serwera wskazując zasób (URL) i operację (metoda HTTP), serwer zwraca reprezentację zasobu (najczęściej JSON). REST nie jest protokołem — to zbiór konwencji: zasoby jako rzeczowniki w URL, standaryzowane metody HTTP, odpowiednie kody statusu. Większość API w praktyce to "REST-like" HTTP API, nie idealnie zgodne z akademicką definicją.

Jaka jest różnica między PUT a PATCH?

PUT zamienia cały zasób — wysyłasz pełną reprezentację zasobu, serwer go zastępuje. Brakujące pola mogą być wyzerowane lub usunięte. PATCH aktualizuje tylko przekazane pola — wysyłasz tylko to, co chcesz zmienić, reszta pozostaje bez zmian. W praktyce: PATCH jest wygodniejszy do cząstkowych aktualizacji (zmiana emaila, statusu), PUT przy zastąpieniu całego obiektu.

Jaka jest różnica między 401 a 403?

401 Unauthorized — brak autentykacji: użytkownik nie jest zalogowany, token jest nieważny lub wygasł. Nazwa jest myląca — w praktyce chodzi o autentykację, nie autoryzację. 403 Forbidden — brak uprawnień: użytkownik jest zaautentykowany, ale nie ma prawa do tej operacji (np. zwykły user próbuje usunąć cudzego posta). Prosta reguła: 401 = "zaloguj się", 403 = "nie wolno Ci".

REST vs GraphQL — co wybrać?

REST jest prostszy w implementacji, lepiej cachuje (GET do CDN, czyli Content Delivery Network, przyspiesza dostarczanie zasobów z serwerów bliższych użytkownikowi.), jest bardziej przewidywalny i ma szerokie wsparcie narzędzi. GraphQL daje klientowi kontrolę nad kształtem danych (mniej over/under-fetchingu), lepiej sprawdza się przy złożonych, połączonych danych i wielu frontendach o różnych potrzebach. Dla większości aplikacji REST jest wystarczający i prostszy w utrzymaniu. GraphQL warto rozważyć przy publicznym API z wieloma konsumentami lub dużej złożoności grafu danych.

Jak wersjonować REST API?

Najpopularniejsza i najbardziej widoczna metoda to wersjonowanie w URL: /api/v1/users, /api/v2/users. Alternatywy to wersjonowanie przez nagłówek Accept: application/vnd.app.v2+json lub query parameter ?version=2. Praktyczna zasada: zacznij od /v1 już przy pierwszym publicznym release'ie — dodanie wersjonowania do istniejącego, bezwersyjnego API jest samo w sobie breaking change dla wszystkich jego konsumentów.

Co to jest idempotencja i dlaczego ważna w API?

Idempotentna operacja to taka, którą można wywołać wielokrotnie z tym samym skutkiem, czyli GET, PUT, DELETE są idempotentne, a ich kilkukrotne wywołanie nie zmienia efektu. POST nie jest idempotentny, ponieważ każde wywołanie tworzy nowy zasób. Praktyczne zastosowanie jest takie, że przy timeoutach i retriable requestach idempotency key (unikalny identyfikator requestu w nagłówku) pozwala serwerowi rozpoznać duplikat i nie wykonać operacji drugi raz, co jest kluczowe przy płatnościach.

Jak obsługiwać błędy w REST API?

Dobry format błędu zawiera: kod HTTP (4xx dla błędów klienta, 5xx dla serwera), maszynoczytelny kod błędu (np. VALIDATION_ERROR, NOT_FOUND), komunikat dla developera i opcjonalnie szczegóły (lista błędów walidacji z polami). Unikaj zwracania 200 OK z błędem w body, ponieważ to utrudnia obsługę i cache. Bądź przy tym konsekwentny w formacie błędów przez całe API, bo jak pokazuje doświadczenie, to częsty problem.

Podsumowanie — checklist

URL

  • Rzeczowniki, nie czasowniki,
  • Liczba mnoga,
  • Kebab-case,
  • Max 2-3 poziomy zagnieżdżenia.

Metody HTTP

  • GET — pobieranie,
  • POST — tworzenie,
  • PUT — pełna aktualizacja,
  • PATCH — częściowa aktualizacja,
  • DELETE — usuwanie.

Kody statusu

  • 200/201/204 dla sukcesu,
  • 400/401/403/404/422 dla błędów klienta,
  • 500 dla błędów serwera.

Dodatkowe

REST to konwencje, a nie sztywne reguły, przy czym najważniejsze to być konsekwentnym w całym API.


Chcesz zbudować własne API? Sprawdź Node.js — pierwszy serwer w 30 minut lub zbuduj fullstack app z Prisma + Next.js.

Pracuję z tym zawodowo.

Jeśli chcesz uporządkować backendowe fundamenty aplikacji i uniknąć typowych błędów architektonicznych już na starcie, skontaktuj się ze mną. Pomagam przekładać wiedzę techniczną na rozwiązania, które da się sensownie utrzymać i rozwijać.

O autorze

Maciej Sala

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

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

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab