REST API Best Practices - Kompletny Przewodnik dla Developerów 2026
Projektowanie REST API to nie jest tylko "napisz endpoint". To sztuka balansowania między prostotą dla klientów, wydajnością, bezpieczeństwem i możliwością ewolucji. Źle zaprojektowane API staje się długiem technicznym - trudne do zmiany, źle udokumentowane, nieintuicyjne dla developerów.
Ten przewodnik to kompletne kompendium REST API best practices - od podstaw przez metody HTTP, kody statusu, paginację, autentykację, po optymalizację wydajności i dokumentację. Wszystko co rekruterzy sprawdzają na rozmowach backend.
Podstawy REST - Architektura i Zasady
REST (Representational State Transfer) to styl architektoniczny zdefiniowany przez Roya Fieldinga w 2000 roku. Nie jest to protokół ani standard - to zbiór ograniczeń (constraints), które nadają API określone właściwości.
6 Zasad REST
1. Client-Server - oddzielenie klienta od serwera. Klient nie musi wiedzieć jak działa backend, serwer nie musi wiedzieć jaki jest frontend. Pozwala na niezależny rozwój obu stron.
2. Stateless - każdy request musi zawierać wszystkie informacje potrzebne do jego przetworzenia. Serwer nie przechowuje stanu sesji klienta między requestami. Ułatwia skalowanie (każdy serwer może obsłużyć każdy request).
3. Cacheable - odpowiedzi muszą definiować czy mogą być cache'owane. Poprawia wydajność i skalowalność.
4. Uniform Interface - spójny interfejs oparty na zasobach:
- Identyfikacja zasobów (URI)
- Manipulacja zasobów przez reprezentacje
- Samo-opisujące się wiadomości
- HATEOAS (Hypermedia as the Engine of Application State)
5. Layered System - możliwość dodania warstw pośrednich (load balancer, cache, gateway) bez wiedzy klienta.
6. Code on Demand (opcjonalne) - serwer może dostarczyć wykonywalny kod (np. JavaScript).
Richardson Maturity Model
Model określa poziomy dojrzałości REST API:
| Poziom | Nazwa | Opis |
|---|---|---|
| 0 | The Swamp of POX | Jeden endpoint, XML/JSON, RPC-style |
| 1 | Resources | Wiele URI, jeden zasób per endpoint |
| 2 | HTTP Verbs | Właściwe użycie GET, POST, PUT, DELETE |
| 3 | Hypermedia Controls | HATEOAS - linki w response |
Większość API zatrzymuje się na poziomie 2. Poziom 3 (HATEOAS) jest rzadko implementowany w praktyce.
REST vs SOAP vs GraphQL
REST:
+ Prostota, wykorzystuje HTTP
+ Cache-friendly
+ Szeroka adopcja
- Over-fetching/under-fetching
- Wiele roundtripów dla powiązanych danych
SOAP:
+ Wbudowane bezpieczeństwo (WS-Security)
+ Transakcje, kontrakty WSDL
- Verbose XML
- Większy overhead
GraphQL:
+ Jedno query = wszystkie dane
+ Typowany schemat
+ Brak over/under-fetching
- Złożoność cache'owania
- Trudniejsza optymalizacja
Metody HTTP - Semantyka i Idempotentność
Każda metoda HTTP ma określoną semantykę. Jej prawidłowe użycie to fundament REST API.
Metody CRUD
POST /api/users Create - tworzenie nowego zasobu
GET /api/users Read - pobieranie listy zasobów
GET /api/users/123 Read - pobieranie pojedynczego zasobu
PUT /api/users/123 Update - pełna aktualizacja zasobu
PATCH /api/users/123 Update - częściowa aktualizacja
DELETE /api/users/123 Delete - usunięcie zasobu
PUT vs PATCH - Kluczowa Różnica
PUT - zastępuje CAŁY zasób:
// Aktualny stan zasobu
{
"id": 123,
"name": "Jan",
"email": "jan@example.com",
"role": "user"
}
// PUT /api/users/123
// Wysyłasz kompletny obiekt
{
"name": "Jan Kowalski",
"email": "jan@example.com",
"role": "user"
}
// Wynik: pełna zamiana
PATCH - modyfikuje WYBRANE pola:
// PATCH /api/users/123
// Wysyłasz tylko to co chcesz zmienić
{
"name": "Jan Kowalski"
}
// Wynik: tylko name się zmienia, reszta bez zmian
Kiedy który używać?
- PUT: gdy masz pełny obiekt i chcesz go nadpisać
- PATCH: gdy aktualizujesz pojedyncze pola
Idempotentność
Operacja jest idempotentna gdy wielokrotne jej wykonanie daje ten sam efekt co jednokrotne.
| Metoda | Idempotentna | Bezpieczna | Opis |
|---|---|---|---|
| GET | Tak | Tak | Tylko odczyt |
| HEAD | Tak | Tak | Jak GET ale bez body |
| OPTIONS | Tak | Tak | Zwraca dozwolone metody |
| PUT | Tak | Nie | Wielokrotne PUT daje ten sam stan |
| DELETE | Tak | Nie | Drugie DELETE zwraca 404 |
| POST | Nie | Nie | Każde POST może tworzyć nowy zasób |
| PATCH | Zależy | Nie | Może być idempotentne |
Dlaczego to ważne? Klient może bezpiecznie retry'ować idempotentne operacje przy błędach sieci.
POST vs PUT do Tworzenia
POST /api/users # Serwer generuje ID
{
"name": "Jan"
}
// Response: 201 Created, Location: /api/users/123
PUT /api/users/123 # Klient określa ID
{
"name": "Jan"
}
// Response: 201 Created lub 200 OK
Reguła: POST gdy serwer generuje identyfikator, PUT gdy klient go zna.
Projektowanie URL i Zasobów
Dobre URL są intuicyjne, przewidywalne i samo-dokumentujące się.
Best Practices dla URL
1. Używaj rzeczowników, nie czasowników:
DOBRZE: GET /api/users
ŹLE: GET /api/getUsers
DOBRZE: POST /api/users
ŹLE: POST /api/createUser
2. Liczba mnoga dla kolekcji:
GET /api/users # Kolekcja
GET /api/users/123 # Pojedynczy zasób
Nie: /api/user, /api/user/123
3. Hierarchia dla zagnieżdżonych zasobów:
GET /api/users/123/orders # Zamówienia użytkownika 123
GET /api/users/123/orders/456 # Zamówienie 456 użytkownika 123
POST /api/users/123/orders # Nowe zamówienie dla użytkownika 123
4. Maksymalnie 2-3 poziomy zagnieżdżenia:
DOBRZE: /api/users/123/orders
ŹLE: /api/users/123/orders/456/items/789/details
5. Kebab-case dla wieloczłonowych nazw:
DOBRZE: /api/order-items
ŹLE: /api/orderItems, /api/order_items
Query Parameters vs Path Parameters
Path parameters - identyfikują konkretny zasób:
GET /api/users/123 # User o ID 123
GET /api/products/laptop-dell # Produkt po slug
Query parameters - filtrowanie, sortowanie, paginacja:
GET /api/users?status=active
GET /api/users?role=admin&sort=-createdAt
GET /api/users?page=2&limit=20
GET /api/products?category=electronics&min_price=100
Akcje Niestandardowe
Czasem REST nie wystarczy. Jak obsłużyć akcje jak "aktywuj użytkownika" czy "wyślij email"?
Opcja 1: PATCH z akcją
PATCH /api/users/123
{ "status": "active" }
Opcja 2: Zagnieżdżony zasób akcji
POST /api/users/123/activation
POST /api/orders/456/cancellation
Opcja 3: Dedykowany endpoint (RPC-style)
POST /api/users/123/activate
POST /api/emails/send
Opcje 1 i 2 są bardziej RESTful, opcja 3 jest bardziej intuicyjna dla developerów.
Kody Statusu HTTP
Prawidłowe kody statusu komunikują wynik operacji bez potrzeby parsowania body.
Najważniejsze Kody
2xx - Sukces:
200 OK - Sukces GET, PUT, PATCH
201 Created - Sukces POST (zasób utworzony)
202 Accepted - Request przyjęty do przetworzenia (async)
204 No Content - Sukces DELETE, PUT bez body
3xx - Przekierowania:
301 Moved Permanently - Zasób przeniesiony na stałe
304 Not Modified - Cache jest aktualny
4xx - Błędy Klienta:
400 Bad Request - Nieprawidłowa składnia requestu
401 Unauthorized - Brak uwierzytelnienia
403 Forbidden - Brak autoryzacji
404 Not Found - Zasób nie istnieje
405 Method Not Allowed - Metoda niedozwolona dla zasobu
409 Conflict - Konflikt (np. duplikat)
422 Unprocessable Entity - Walidacja nie przeszła
429 Too Many Requests - Rate limit przekroczony
5xx - Błędy Serwera:
500 Internal Server Error - Nieoczekiwany błąd
502 Bad Gateway - Błąd proxy/gateway
503 Service Unavailable - Serwis chwilowo niedostępny
504 Gateway Timeout - Timeout na proxy
401 vs 403 - Częste Pytanie
// 401 Unauthorized - brak tokena lub token nieprawidłowy
// "Nie wiem kim jesteś, zaloguj się"
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
// 403 Forbidden - token ok, ale brak uprawnień
// "Wiem kim jesteś, ale nie masz dostępu"
if (user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
400 vs 422 - Walidacja
// 400 Bad Request - nieprawidłowy JSON, brak wymaganych pól
// Problem ze składnią requestu
{
"error": "Invalid JSON syntax"
}
// 422 Unprocessable Entity - JSON poprawny, ale dane nie przeszły walidacji
// Request zrozumiany, ale semantycznie niepoprawny
{
"error": "Validation failed",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "password", "message": "Must be at least 8 characters" }
]
}
Response dla Błędów
Ustandaryzuj format błędów w całym API:
// Error response format
{
"error": {
"code": "VALIDATION_ERROR", // Kod do programistycznego użycia
"message": "Validation failed", // Czytelny opis
"details": [ // Szczegóły (opcjonalne)
{ "field": "email", "message": "Invalid format" }
],
"timestamp": "2026-01-08T10:30:00Z",
"requestId": "abc-123-def" // Do debugowania
}
}
Paginacja
Paginacja jest niezbędna dla endpointów zwracających kolekcje. Bez niej duże zbiory danych zabiją wydajność.
Offset Pagination
Najprostsza, ale ma problemy:
GET /api/users?page=2&limit=20
GET /api/users?offset=20&limit=20
// Response
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 1500,
"totalPages": 75
}
}
Problemy offset pagination:
-
Wydajność -
OFFSET 100000jest wolny (DB musi przeskanować i pominąć wiersze) - Niestabilność - dodanie/usunięcie elementu przesuwa wszystkie strony
- Duplikaty/braki - przy zmianach danych podczas paginacji
Cursor Pagination
Używa nieprzejrzystego kursora zamiast numeru strony:
GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=20
// Response
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIwfQ",
"prevCursor": "eyJpZCI6ODV9",
"hasMore": true
}
}
Implementacja:
// Cursor to zakodowany identyfikator ostatniego elementu
const cursor = Buffer.from(JSON.stringify({ id: lastItem.id }))
.toString('base64');
// Dekodowanie i query
const { id } = JSON.parse(Buffer.from(cursor, 'base64').toString());
const items = await db.query(
'SELECT * FROM users WHERE id > $1 ORDER BY id LIMIT $2',
[id, limit + 1]
);
Zalety cursor:
- Wydajny (używa indeksów)
- Stabilny przy zmianach danych
- Działa dla nieskończonego scrollowania
Wady:
- Brak "skoku" do strony N
- Bardziej skomplikowana implementacja
Keyset Pagination
Podobna do cursor, ale używa jawnych wartości:
GET /api/users?after_id=100&limit=20
GET /api/posts?after_created_at=2026-01-08T10:00:00Z&limit=20
Wymaga unikalnego, sortowalnego pola (ID, timestamp).
Która Strategia Kiedy?
| Strategia | Kiedy używać |
|---|---|
| Offset | Małe zbiory (<10k), potrzeba skoku do strony |
| Cursor | Duże zbiory, real-time dane, infinite scroll |
| Keyset | Proste przypadki cursor, publiczne API |
Autentykacja i Autoryzacja
Bezpieczeństwo API to podstawa. Musisz znać różne metody autentykacji.
JWT (JSON Web Token)
Najpopularniejsza metoda dla REST API:
const jwt = require('jsonwebtoken');
// Generowanie tokena
const generateToken = (user) => {
return jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
};
// Weryfikacja
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
Struktura JWT:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.signature
|______ Header ______|_____ Payload _____|_ Signature _|
Access Token + Refresh Token
// Login - zwraca oba tokeny
const accessToken = generateToken(user, '15m');
const refreshToken = generateToken(user, '7d');
// Access token w Authorization header
Authorization: Bearer <access_token>
// Refresh token w httpOnly cookie lub osobnym endpoincie
POST /api/auth/refresh
Cookie: refreshToken=<refresh_token>
// Response
{
"accessToken": "<new_access_token>"
}
Dlaczego dwa tokeny?
- Access token: krótki (15min), w pamięci, ryzyko wycieku ograniczone
- Refresh token: długi (dni), httpOnly cookie, do odświeżania access
OAuth 2.0
Dla integracji z zewnętrznymi serwisami (Google, Facebook):
1. Redirect do authorization server:
GET https://auth.google.com/authorize?
client_id=xxx&
redirect_uri=https://myapp.com/callback&
scope=email profile&
response_type=code
2. User loguje się i akceptuje
3. Redirect z code:
GET https://myapp.com/callback?code=abc123
4. Wymiana code na token (server-to-server):
POST https://auth.google.com/token
{ code, client_id, client_secret }
5. Response:
{ access_token, refresh_token, expires_in }
API Keys
Prostsze niż JWT, dobre dla server-to-server:
GET /api/data
X-API-Key: sk_live_abcd1234
// lub w query param (mniej bezpieczne)
GET /api/data?api_key=sk_live_abcd1234
Dobre praktyki API keys:
- Prefix określający typ (sk_ = secret, pk_ = public)
- Możliwość rotacji bez przerwy
- Ograniczenie per IP/domena
- Nie logować w plain text
Bezpieczeństwo API
Rate Limiting
Ochrona przed nadużyciami:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minuta
max: 100, // 100 requestów per window
message: {
error: 'Too many requests',
retryAfter: 60
},
standardHeaders: true, // RateLimit-* headers
legacyHeaders: false
});
app.use('/api/', limiter);
Response headers:
RateLimit-Limit: 100
RateLimit-Remaining: 45
RateLimit-Reset: 1704715800
Retry-After: 60
CORS (Cross-Origin Resource Sharing)
const cors = require('cors');
app.use(cors({
origin: ['https://myapp.com', 'https://admin.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // Pozwól na cookies
maxAge: 86400 // Preflight cache: 24h
}));
Preflight request:
OPTIONS /api/users
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Walidacja i Sanityzacja
const Joi = require('joi');
const sanitizeHtml = require('sanitize-html');
// Schema walidacji
const createUserSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/[A-Z]/).pattern(/[0-9]/).required()
});
// Middleware walidacji
const validate = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
return res.status(422).json({
error: 'Validation failed',
details: error.details.map(d => ({
field: d.path.join('.'),
message: d.message
}))
});
}
next();
};
// Sanityzacja HTML (ochrona przed XSS)
const sanitizeInput = (input) => {
return sanitizeHtml(input, {
allowedTags: [],
allowedAttributes: {}
});
};
OWASP Top 10 dla API
- Broken Object Level Authorization - sprawdzaj czy user ma dostęp do zasobu
- Broken Authentication - silne hasła, rate limiting na login
- Excessive Data Exposure - zwracaj tylko potrzebne pola
- Lack of Resources & Rate Limiting - limity requestów
- Broken Function Level Authorization - sprawdzaj role na każdym endpoint
- Mass Assignment - whitelist dozwolonych pól
- Security Misconfiguration - wyłącz debug w produkcji
- Injection - parametryzowane zapytania
- Improper Assets Management - dokumentuj wszystkie endpointy
- Insufficient Logging & Monitoring - loguj i monitoruj
Wydajność i Caching
HTTP Caching
// Cache-Control header
app.get('/api/products', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=3600', // Cache 1h
'ETag': '"abc123"' // Dla conditional requests
});
res.json(products);
});
// Dla danych użytkownika
app.get('/api/users/me', (req, res) => {
res.set({
'Cache-Control': 'private, no-cache', // Nie cache'uj publicznie
'ETag': `"user-${user.id}-${user.updatedAt}"`
});
res.json(user);
});
ETag i Conditional Requests
// Request z If-None-Match
// GET /api/products
// If-None-Match: "abc123"
app.get('/api/products', (req, res) => {
const etag = generateETag(products);
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // Not Modified
}
res.set('ETag', etag);
res.json(products);
});
Kompresja
const compression = require('compression');
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
threshold: 1024, // Nie kompresuj < 1KB
level: 6 // Balans między szybkością a rozmiarem
}));
Partial Response (Fields Selection)
Pozwól klientom żądać tylko potrzebnych pól:
GET /api/users?fields=id,name,email
app.get('/api/users', (req, res) => {
const fields = req.query.fields?.split(',');
let query = User.find();
if (fields) {
query = query.select(fields.join(' '));
}
const users = await query;
res.json(users);
});
Dokumentacja - OpenAPI/Swagger
OpenAPI Specification
openapi: 3.0.3
info:
title: My REST API
version: 1.0.0
description: API for managing users
servers:
- url: https://api.myapp.com/v1
paths:
/users:
get:
summary: Get all users
parameters:
- name: status
in: query
schema:
type: string
enum: [active, inactive]
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: List of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUser'
responses:
'201':
description: User created
'422':
description: Validation error
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
createdAt:
type: string
format: date-time
CreateUser:
type: object
required:
- name
- email
- password
properties:
name:
type: string
minLength: 2
email:
type: string
format: email
password:
type: string
minLength: 8
Best Practices Dokumentacji
- Dokumentuj wszystkie endpointy - nie zostawiaj "ukrytych" API
- Przykłady request/response - konkretne, nie abstrakcyjne
- Kody błędów - wszystkie możliwe błędy z opisami
- Wersjonowanie docs - osobna dokumentacja per wersja API
- Interaktywne testowanie - Swagger UI, Postman collections
- Changelog - co się zmieniło między wersjami
Testowanie REST API
Unit Tests
describe('UsersController', () => {
describe('POST /users', () => {
it('should create user with valid data', async () => {
const userData = { name: 'Jan', email: 'jan@example.com', password: 'Secret123' };
const result = await usersController.create(userData);
expect(result.name).toBe('Jan');
expect(result.email).toBe('jan@example.com');
expect(result.password).toBeUndefined(); // Nie zwracaj hasła
});
it('should reject duplicate email', async () => {
usersRepository.findByEmail.mockResolvedValue({ id: 1 });
await expect(usersController.create(userData))
.rejects.toThrow(ConflictException);
});
});
});
Integration Tests
const request = require('supertest');
describe('Users API', () => {
it('POST /api/users - creates user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Jan', email: 'jan@example.com', password: 'Secret123' })
.expect(201);
expect(response.body.id).toBeDefined();
expect(response.body.email).toBe('jan@example.com');
});
it('POST /api/users - rejects invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Jan', email: 'invalid', password: 'Secret123' })
.expect(422);
expect(response.body.error).toBe('Validation failed');
});
it('GET /api/users - requires authentication', async () => {
await request(app)
.get('/api/users')
.expect(401);
});
it('GET /api/users - returns users with valid token', async () => {
const response = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${validToken}`)
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
Contract Testing
Weryfikacja że API spełnia kontrakt (OpenAPI spec):
const SwaggerParser = require('@apidevtools/swagger-parser');
const { OpenApiValidator } = require('express-openapi-validate');
const spec = await SwaggerParser.bundle('./openapi.yaml');
const validator = new OpenApiValidator(spec);
it('response matches OpenAPI spec', async () => {
const response = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${token}`);
const valid = validator.validateResponse('get', '/users', response);
expect(valid.errors).toHaveLength(0);
});
Podsumowanie: REST API Checklist
Projektowanie URL
- Rzeczowniki, nie czasowniki
- Liczba mnoga dla kolekcji
- Konsekwentne nazewnictwo (kebab-case)
- Max 2-3 poziomy zagnieżdżenia
Metody HTTP
- Właściwe metody dla operacji
- PUT dla pełnej aktualizacji, PATCH dla częściowej
- Idempotentność gdzie potrzebna
Response
- Prawidłowe kody statusu
- Spójny format błędów
- Paginacja dla list
- Tylko potrzebne dane
Bezpieczeństwo
- HTTPS
- JWT/OAuth dla autentykacji
- Rate limiting
- Walidacja inputu
- CORS skonfigurowany
Wydajność
- Cache headers
- Kompresja
- Fields selection
- Efektywna paginacja
Dokumentacja
- OpenAPI/Swagger
- Przykłady
- Kody błędów
- Changelog
Zobacz też
- Kompletny Przewodnik - Rozmowa Node.js Backend Developer - pełna mapa wiedzy backend
- Express.js - Pytania Rekrutacyjne - middleware, routing, REST w Express
- NestJS - Pytania Rekrutacyjne - REST w NestJS
- Autentykacja i JWT - Pytania Rekrutacyjne - deep dive w JWT
Chcesz Więcej Pytań o REST API?
Mamy 50 pytań rekrutacyjnych o REST API best practices - od podstaw przez bezpieczeństwo po zaawansowane wzorce. Każde z pełną odpowiedzią w formacie "30 sekund / 2 minuty".
Zdobądź Pełny Dostęp do Wszystkich Pytań
Napisane przez zespół Flipcards, na podstawie doświadczeń z projektowania i review API w projektach enterprise.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
