Zrozumienie zasad REST to podstawa projektowania API, które są:
- intuicyjne w użyciu,
- łatwe do integracji,
- zgodne z oczekiwaniami społeczności deweloperskiej,
- i gotowe na skalowanie.
W tym przewodniku poznasz zasady projektowania REST API, metody HTTP, kody statusu, strukturę odpowiedzi, wersjonowanie i najlepsze praktyki implementacji w Pythonie z użyciem FastAPI, Flask i Django REST Framework.
Kluczowe zasady REST API
REST API opiera się na kilku fundamentalnych zasadach, które czynią je prostymi, skalowalnymi i łatwymi w użyciu:
1. Statelessness
Każde żądanie jest niezależne i zawiera wszystkie dane potrzebne do obsługi. Serwer nie przechowuje stanu sesji między zapytaniami. To oznacza, że:
- Każde żądanie musi zawierać wszystkie informacje potrzebne do jego przetworzenia.
- Brak zależności od poprzednich żądań.
- Łatwiejsze skalowanie — każdy request może być obsłużony przez dowolny serwer.
Przykład:
Bash1 2GET /api/users/123 HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Token autoryzacji jest przesyłany w każdym żądaniu — serwer nie musi pamiętać, kto jest zalogowany.
2. Użycie standardowych metod HTTP
Każda metoda ma określone znaczenie semantyczne:
- GET — pobieranie danych (bez zmian w systemie)
- POST — tworzenie nowych zasobów
- PUT — pełna aktualizacja zasobu
- PATCH — częściowa aktualizacja zasobu
- DELETE — usuwanie zasobu
3. Zasoby (Resources)
Dane są reprezentowane jako zasoby identyfikowane przez URI. Zasoby powinny być rzeczownikami w liczbie mnogiej:
Bash1 2 3 4/api/users # Lista użytkowników /api/users/123 # Konkretny użytkownik /api/users/123/orders # Zamówienia użytkownika /api/orders/456/items # Elementy zamówienia
Dobre praktyki:
- Używaj rzeczowników, nie czasowników:
/userszamiast/getUsers - Używaj liczby mnogiej:
/userszamiast/user - Unikaj zbyt głębokiego zagnieżdżania:
/users/1/orders/5/items/2może być zbyt skomplikowane
4. Użycie standardowych kodów odpowiedzi HTTP
API powinno zwracać jednoznaczne kody statusu zgodne ze standardami HTTP. Klient wie, co się stało bez czytania treści odpowiedzi.
5. Format danych
Najczęściej JSON (application/json), czasem XML. JSON jest standardem w nowoczesnych API ze względu na prostotę i szerokie wsparcie.
6. HATEOAS (Hypermedia as the Engine of Application State)
Odpowiedzi mogą zawierać linki do powiązanych zasobów. Nie zawsze stosowane, ale zgodne z duchem REST:
Bash1 2 3 4 5 6 7 8{ "id": 1, "name": "Kacper", "links": { "self": "/api/users/1", "orders": "/api/users/1/orders" } }
Metody HTTP w REST API
Każda metoda HTTP ma swoje semantyczne znaczenie i powinna być używana zgodnie z konwencją:
| Metoda | Opis | Idempotentna? | Bezpieczna? | Przykład |
|---|---|---|---|---|
| GET | Pobiera zasób lub listę zasobów | Tak | Tak | GET /api/users |
| POST | Tworzy nowy zasób | Nie | Nie | POST /api/users |
| PUT | Zastępuje cały zasób (lub tworzy, jeśli nie istnieje) | Tak | Nie | PUT /api/users/1 |
| PATCH | Aktualizuje część zasobu | Tak* | Nie | PATCH /api/users/1 |
| DELETE | Usuwa zasób | Tak | Nie | DELETE /api/users/1 |
*PATCH może nie być idempotentna, jeśli aktualizacja jest relatywna (np. {"count": "+1"})
Przykłady użycia metod
GET - Pobranie listy:
HTTP1GET /api/users HTTP/1.1
GET - Pobranie pojedynczego zasobu:
HTTP1GET /api/users/123 HTTP/1.1
POST - Utworzenie nowego zasobu:
HTTP1 2 3 4 5 6 7POST /api/users HTTP/1.1 Content-Type: application/json { "name": "Kacper", "email": "kacper@example.com" }
PUT - Pełna aktualizacja:
HTTP1 2 3 4 5 6 7 8PUT /api/users/123 HTTP/1.1 Content-Type: application/json { "name": "Kacper Sieradziński", "email": "kacper@example.com", "role": "admin" }
PATCH - Częściowa aktualizacja:
HTTP1 2 3 4 5 6PATCH /api/users/123 HTTP/1.1 Content-Type: application/json { "email": "nowy@example.com" }
DELETE - Usunięcie:
HTTP1DELETE /api/users/123 HTTP/1.1
Kody statusu HTTP
Prawidłowe kody odpowiedzi to klucz do dobrego API. Nie wymyślaj własnych — korzystaj ze standardów:
| Kod | Znaczenie | Użycie | Przykład |
|---|---|---|---|
| 200 OK | Operacja zakończona powodzeniem | GET, PUT, PATCH | Pobranie zasobu |
| 201 Created | Utworzono nowy zasób | POST | Utworzenie użytkownika |
| 204 No Content | Operacja udana, brak treści | DELETE, PUT | Usunięcie zasobu |
| 400 Bad Request | Błąd w danych wejściowych | Wszystkie | Nieprawidłowa walidacja |
| 401 Unauthorized | Brak autoryzacji | Wszystkie | Brak tokena JWT |
| 403 Forbidden | Brak uprawnień | Wszystkie | Brak uprawnień do zasobu |
| 404 Not Found | Nie znaleziono zasobu | GET, PUT, PATCH, DELETE | Błędny ID |
| 409 Conflict | Konflikt danych | POST, PUT, PATCH | Duplikat unikalnej wartości |
| 422 Unprocessable Entity | Dane poprawne składniowo, ale nieprawidłowe semantycznie | POST, PUT, PATCH | Błąd walidacji biznesowej |
| 429 Too Many Requests | Przekroczony limit żądań | Wszystkie | Rate limiting |
| 500 Internal Server Error | Błąd serwera | Wszystkie | Nieoczekiwany wyjątek |
Przykłady użycia kodów statusu
Sukces:
HTTP1 2 3 4 5 6 7 8HTTP/1.1 201 Created Content-Type: application/json { "id": 123, "name": "Kacper", "email": "kacper@example.com" }
Błąd walidacji:
HTTP1 2 3 4 5 6 7 8 9HTTP/1.1 400 Bad Request Content-Type: application/json { "error": "Validation failed", "details": { "email": "Invalid email format" } }
Nie znaleziono:
HTTP1 2 3 4 5 6 7HTTP/1.1 404 Not Found Content-Type: application/json { "error": "User not found", "message": "User with id 123 does not exist" }
Struktura odpowiedzi JSON
Odpowiedzi z API powinny być spójne i przewidywalne. Użyj jednolitej struktury w całym API.
Standardowa struktura odpowiedzi sukcesu
JSON1 2 3 4 5 6 7 8{ "status": "success", "data": { "id": 1, "name": "Kacper", "email": "kacper@example.com" } }
Lista zasobów z paginacją
JSON1 2 3 4 5 6 7 8 9 10 11 12 13{ "status": "success", "data": [ {"id": 1, "name": "Kacper"}, {"id": 2, "name": "Anna"} ], "meta": { "page": 1, "limit": 20, "total": 150, "total_pages": 8 } }
Struktura odpowiedzi błędu
JSON1 2 3 4 5 6 7 8 9 10{ "status": "error", "error": { "code": 400, "message": "Invalid email address", "details": { "email": "Email must be a valid format" } } }
Zalecana struktura
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18# Funkcja pomocnicza do formatowania odpowiedzi def success_response(data, status_code=200): return { "status": "success", "data": data }, status_code def error_response(message, code=400, details=None): response = { "status": "error", "error": { "code": code, "message": message } } if details: response["error"]["details"] = details return response, code
REST API w praktyce (FastAPI)
FastAPI to nowoczesny framework do budowy REST API w Pythonie, z automatyczną walidacją i dokumentacją OpenAPI.
Podstawowy przykład
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77from fastapi import FastAPI, HTTPException, status from pydantic import BaseModel, EmailStr from typing import List app = FastAPI(title="Users API", version="1.0.0") class User(BaseModel): id: int name: str email: EmailStr class UserCreate(BaseModel): name: str email: EmailStr # Tymczasowa "baza danych" users_db = [ User(id=1, name="Kacper", email="kacper@example.com"), User(id=2, name="Anna", email="anna@example.com") ] @app.get("/api/users", response_model=List[User]) def list_users(): """Pobierz listę wszystkich użytkowników""" return users_db @app.get("/api/users/{user_id}", response_model=User) def get_user(user_id: int): """Pobierz użytkownika po ID""" user = next((u for u in users_db if u.id == user_id), None) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with id {user_id} not found" ) return user @app.post("/api/users", status_code=status.HTTP_201_CREATED, response_model=User) def create_user(user: UserCreate): """Utwórz nowego użytkownika""" new_id = max([u.id for u in users_db], default=0) + 1 new_user = User(id=new_id, **user.dict()) users_db.append(new_user) return new_user @app.put("/api/users/{user_id}", response_model=User) def update_user(user_id: int, user: UserCreate): """Zaktualizuj użytkownika (pełna aktualizacja)""" existing_user = next((u for u in users_db if u.id == user_id), None) if not existing_user: raise HTTPException(status_code=404, detail="User not found") existing_user.name = user.name existing_user.email = user.email return existing_user @app.patch("/api/users/{user_id}", response_model=User) def partial_update_user(user_id: int, user: dict): """Zaktualizuj część danych użytkownika""" existing_user = next((u for u in users_db if u.id == user_id), None) if not existing_user: raise HTTPException(status_code=404, detail="User not found") for key, value in user.items(): setattr(existing_user, key, value) return existing_user @app.delete("/api/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_user(user_id: int): """Usuń użytkownika""" global users_db user = next((u for u in users_db if u.id == user_id), None) if not user: raise HTTPException(status_code=404, detail="User not found") users_db = [u for u in users_db if u.id != user_id] return None
Ten przykład pokazuje podstawy REST: GET, POST, PUT, PATCH, DELETE, odpowiednie kody statusu i automatyczną walidację przez Pydantic.
REST API w Django REST Framework
Django REST Framework (DRF) dostarcza kompletne narzędzia do tworzenia API: serializatory, widoki, autoryzację, automatyczną dokumentację.
Model i Serializer
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17# models.py from django.db import models class User(models.Model): name = models.CharField(max_length=100) email = models.EmailField(unique=True) created_at = models.DateTimeField(auto_now_add=True) # serializers.py from rest_framework import serializers from .models import User class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['id', 'name', 'email', 'created_at'] read_only_fields = ['id', 'created_at']
ViewSet
Python1 2 3 4 5 6 7 8# views.py from rest_framework import viewsets from .models import User from .serializers import UserSerializer class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer
Routing
Python1 2 3 4 5 6 7 8# urls.py from rest_framework.routers import DefaultRouter from .views import UserViewSet router = DefaultRouter() router.register(r'users', UserViewSet, basename='user') urlpatterns = router.urls
Automatycznie tworzy endpointy:
GET /api/users/— lista użytkownikówPOST /api/users/— utworzenie użytkownikaGET /api/users/1/— szczegóły użytkownikaPUT /api/users/1/— pełna aktualizacjaPATCH /api/users/1/— częściowa aktualizacjaDELETE /api/users/1/— usunięcie użytkownika
Paginacja w DRF
Python1 2 3 4 5# settings.py REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20 }
REST API w Flask
Flask jest prostszy i daje więcej kontroli nad strukturą API:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47from flask import Flask, jsonify, request from flask.views import MethodView app = Flask(__name__) users = [ {"id": 1, "name": "Kacper", "email": "kacper@example.com"}, {"id": 2, "name": "Anna", "email": "anna@example.com"} ] class UserAPI(MethodView): def get(self, user_id=None): if user_id: user = next((u for u in users if u["id"] == user_id), None) if not user: return jsonify({"error": "User not found"}), 404 return jsonify(user) return jsonify(users) def post(self): data = request.json new_id = max([u["id"] for u in users], default=0) + 1 new_user = {"id": new_id, **data} users.append(new_user) return jsonify(new_user), 201 def put(self, user_id): user = next((u for u in users if u["id"] == user_id), None) if not user: return jsonify({"error": "User not found"}), 404 data = request.json user.update(data) return jsonify(user) def delete(self, user_id): global users user = next((u for u in users if u["id"] == user_id), None) if not user: return jsonify({"error": "User not found"}), 404 users = [u for u in users if u["id"] != user_id] return "", 204 # Rejestracja endpointów app.add_url_rule('/api/users', view_func=UserAPI.as_view('users'), methods=['GET', 'POST']) app.add_url_rule('/api/users/<int:user_id>', view_func=UserAPI.as_view('user'), methods=['GET', 'PUT', 'DELETE'])
Wersjonowanie API
API powinno być wersjonowane, by wprowadzać zmiany bez psucia kompatybilności ze starszymi klientami.
Najczęstsze sposoby wersjonowania
1. W ścieżce URL (zalecane):
Bash1 2/api/v1/users /api/v2/users
2. W nagłówku Accept:
HTTP1Accept: application/vnd.myapp.v1+json
3. W parametrze query:
Bash1/api/users?version=1
4. W subdomain:
Bash1v1.api.example.com/users
Implementacja wersjonowania w FastAPI
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17from fastapi import FastAPI, APIRouter app = FastAPI() v1_router = APIRouter(prefix="/api/v1", tags=["v1"]) v2_router = APIRouter(prefix="/api/v2", tags=["v2"]) @v1_router.get("/users") def list_users_v1(): return {"version": "v1", "users": [...]} @v2_router.get("/users") def list_users_v2(): return {"version": "v2", "users": [...], "pagination": {...}} app.include_router(v1_router) app.include_router(v2_router)
Zalecane: prosty wariant ścieżkowy /api/v1/, bo jest najbardziej czytelny i łatwy w implementacji.
Autoryzacja i bezpieczeństwo
Wszystkie endpointy REST powinny być zabezpieczone przez:
1. HTTPS – bezwzględny wymóg
Tokeny i dane wrażliwe muszą być przesyłane tylko przez HTTPS.
2. JWT (JSON Web Tokens) – autoryzacja użytkowników
Python1 2 3 4 5 6 7 8 9from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @app.get("/api/profile") def read_profile(token: str = Depends(oauth2_scheme)): # Token jest automatycznie zweryfikowany return {"msg": "Hello authenticated user"}
3. Rate limiting – ograniczenie liczby żądań
Python1 2 3 4 5 6 7 8 9from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @app.get("/api/users") @limiter.limit("100/minute") def list_users(request: Request): return users
4. CORS – kontrola dostępu z frontendu
Python1 2 3 4 5 6 7 8 9from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["https://example.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
5. Input validation – każda wartość musi być walidowana
FastAPI automatycznie waliduje dane przez Pydantic:
Python1 2 3 4 5 6 7 8 9 10 11from pydantic import BaseModel, EmailStr, validator class UserCreate(BaseModel): name: str email: EmailStr @validator('name') def name_must_not_be_empty(cls, v): if not v.strip(): raise ValueError('Name cannot be empty') return v
Najlepsze praktyki REST API
1. Korzystaj z rzeczowników w nazwach zasobów
Bash1 2✅ /users ❌ /getUsers
2. Nie przesadzaj z zagnieżdżeniami
Bash1 2✅ /users/1/orders ❌ /users/1/orders/5/items/2/products/10
Jeśli zagnieżdżenie jest zbyt głębokie, rozważ płaski routing:
Bash1✅ /orders?user_id=1
3. Wykorzystuj paginację
Bash1GET /api/users?page=1&limit=50
Implementacja:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20from fastapi import Query @app.get("/api/users") def list_users( page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100) ): skip = (page - 1) * limit total = len(users_db) users = users_db[skip:skip + limit] return { "data": users, "meta": { "page": page, "limit": limit, "total": total, "total_pages": (total + limit - 1) // limit } }
4. Zwracaj metadane
Odpowiedzi powinny zawierać informacje pomocnicze:
total_items— łączna liczba elementówtotal_pages— łączna liczba stronhas_next— czy jest następna stronahas_previous— czy jest poprzednia strona
5. Obsługuj błędy globalnie
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19from fastapi import FastAPI, Request, status from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError app = FastAPI() @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={ "status": "error", "error": { "code": 422, "message": "Validation error", "details": exc.errors() } } )
6. Zawsze zwracaj JSON, nawet w błędach
Python1 2 3 4 5 6 7 8 9 10 11 12@app.exception_handler(Exception) async def general_exception_handler(request: Request, exc: Exception): return JSONResponse( status_code=500, content={ "status": "error", "error": { "code": 500, "message": "Internal server error" } } )
7. Dokumentuj API – OpenAPI/Swagger to standard
FastAPI automatycznie generuje dokumentację pod /docs i /redoc. Django REST Framework ma podobną funkcjonalność.
8. Używaj statusów HTTP zgodnie z konwencją
Nie wymyślaj własnych kodów — używaj standardów HTTP.
9. Ustal spójną strukturę odpowiedzi w całym API
Wszystkie endpointy powinny zwracać dane w jednolitym formacie.
10. Testuj – pytest + requests lub HTTPX
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17import pytest from fastapi.testclient import TestClient client = TestClient(app) def test_get_users(): response = client.get("/api/users") assert response.status_code == 200 assert "data" in response.json() def test_create_user(): response = client.post( "/api/users", json={"name": "Test", "email": "test@example.com"} ) assert response.status_code == 201 assert response.json()["name"] == "Test"
Dokumentacja i testowanie API
Każde profesjonalne API powinno mieć auto-generowaną dokumentację. FastAPI, Flask i Django REST Framework oferują Swaggera lub ReDoc "z pudełka".
FastAPI – automatyczna dokumentacja
/docs— interaktywna dokumentacja Swagger UI/redoc— dokumentacja w stylu OpenAPI ReDoc/openapi.json— schemat OpenAPI w formacie JSON
FastAPI automatycznie generuje dokumentację na podstawie typu endpointów i modeli Pydantic.
Testowanie API
Pytest z FastAPI TestClient:
Bash1pytest --maxfail=1 --disable-warnings -q
HTTPX dla asynchronicznych testów:
Python1 2 3 4 5 6 7 8import pytest import httpx @pytest.mark.asyncio async def test_async_endpoint(): async with httpx.AsyncClient(app=app, base_url="http://test") as client: response = await client.get("/api/users") assert response.status_code == 200
Testy integracyjne z prawym serwerem:
Python1 2 3def test_api_integration(): response = requests.get("http://localhost:8000/api/users") assert response.status_code == 200
Podsumowanie
REST API to kręgosłup nowoczesnych aplikacji webowych. Dzięki przestrzeganiu zasad REST tworzysz API czytelne, łatwe do integracji i zgodne z najlepszymi praktykami branżowymi.
Python oferuje wszystko, czego potrzebujesz:
- FastAPI dla wydajności, automatycznej walidacji i dokumentacji,
- Flask dla prostoty i elastyczności,
- Django REST Framework dla kompletności i integracji z Django.
Kluczowe zasady do zapamiętania:
- Używaj standardowych metod HTTP zgodnie z ich semantyką,
- Zwracaj odpowiednie kody statusu HTTP,
- Utrzymuj spójną strukturę odpowiedzi,
- Wersjonuj API od początku,
- Zabezpiecz wszystkie endpointy (HTTPS, autoryzacja, walidacja),
- Dokumentuj i testuj swoje API.
Dobrze zaprojektowane REST API to fundament, na którym buduje się nowoczesne aplikacje webowe i mobilne. Zacznij od prostych zasad, a z czasem rozbuduj o zaawansowane funkcjonalności zgodnie z potrzebami projektu.



