REST API w Pythonie – zasady i najlepsze praktyki

Kacper Sieradziński
Kacper Sieradziński
1 czerwca 2025Edukacja7 min czytania

REST (Representational State Transfer) to architektoniczny styl tworzenia interfejsów API, który stał się fundamentem nowoczesnych aplikacji webowych. Umożliwia komunikację między klientem a serwerem w sposób prosty, przewidywalny i zgodny ze standardami HTTP.

Obraz główny REST API w Pythonie – zasady i najlepsze praktyki

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:

Bash
1 2 GET /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:

Bash
1 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: /users zamiast /getUsers
  • Używaj liczby mnogiej: /users zamiast /user
  • Unikaj zbyt głębokiego zagnieżdżania: /users/1/orders/5/items/2 moż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:

Bash
1 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ą:

MetodaOpisIdempotentna?Bezpieczna?Przykład
GETPobiera zasób lub listę zasobówTakTakGET /api/users
POSTTworzy nowy zasóbNie Nie POST /api/users
PUTZastępuje cały zasób (lub tworzy, jeśli nie istnieje)TakNie PUT /api/users/1
PATCHAktualizuje część zasobuTak*Nie PATCH /api/users/1
DELETEUsuwa zasóbTakNie 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:

HTTP
1 GET /api/users HTTP/1.1

GET - Pobranie pojedynczego zasobu:

HTTP
1 GET /api/users/123 HTTP/1.1

POST - Utworzenie nowego zasobu:

HTTP
1 2 3 4 5 6 7 POST /api/users HTTP/1.1 Content-Type: application/json { "name": "Kacper", "email": "kacper@example.com" }

PUT - Pełna aktualizacja:

HTTP
1 2 3 4 5 6 7 8 PUT /api/users/123 HTTP/1.1 Content-Type: application/json { "name": "Kacper Sieradziński", "email": "kacper@example.com", "role": "admin" }

PATCH - Częściowa aktualizacja:

HTTP
1 2 3 4 5 6 PATCH /api/users/123 HTTP/1.1 Content-Type: application/json { "email": "nowy@example.com" }

DELETE - Usunięcie:

HTTP
1 DELETE /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:

KodZnaczenieUżyciePrzykład
200 OKOperacja zakończona powodzeniemGET, PUT, PATCHPobranie zasobu
201 CreatedUtworzono nowy zasóbPOSTUtworzenie użytkownika
204 No ContentOperacja udana, brak treściDELETE, PUTUsunięcie zasobu
400 Bad RequestBłąd w danych wejściowychWszystkieNieprawidłowa walidacja
401 UnauthorizedBrak autoryzacjiWszystkieBrak tokena JWT
403 ForbiddenBrak uprawnieńWszystkieBrak uprawnień do zasobu
404 Not FoundNie znaleziono zasobuGET, PUT, PATCH, DELETEBłędny ID
409 ConflictKonflikt danychPOST, PUT, PATCHDuplikat unikalnej wartości
422 Unprocessable EntityDane poprawne składniowo, ale nieprawidłowe semantyczniePOST, PUT, PATCHBłąd walidacji biznesowej
429 Too Many RequestsPrzekroczony limit żądańWszystkieRate limiting
500 Internal Server ErrorBłąd serweraWszystkieNieoczekiwany wyjątek

Przykłady użycia kodów statusu

Sukces:

HTTP
1 2 3 4 5 6 7 8 HTTP/1.1 201 Created Content-Type: application/json { "id": 123, "name": "Kacper", "email": "kacper@example.com" }

Błąd walidacji:

HTTP
1 2 3 4 5 6 7 8 9 HTTP/1.1 400 Bad Request Content-Type: application/json { "error": "Validation failed", "details": { "email": "Invalid email format" } }

Nie znaleziono:

HTTP
1 2 3 4 5 6 7 HTTP/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

JSON
1 2 3 4 5 6 7 8 { "status": "success", "data": { "id": 1, "name": "Kacper", "email": "kacper@example.com" } }

Lista zasobów z paginacją

JSON
1 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

JSON
1 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

Python
1 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

Python
1 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 77 from 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

Python
1 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

Python
1 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

Python
1 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ów
  • POST /api/users/ — utworzenie użytkownika
  • GET /api/users/1/ — szczegóły użytkownika
  • PUT /api/users/1/ — pełna aktualizacja
  • PATCH /api/users/1/ — częściowa aktualizacja
  • DELETE /api/users/1/ — usunięcie użytkownika

Paginacja w DRF

Python
1 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:

Python
1 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 from 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):

Bash
1 2 /api/v1/users /api/v2/users

2. W nagłówku Accept:

HTTP
1 Accept: application/vnd.myapp.v1+json

3. W parametrze query:

Bash
1 /api/users?version=1

4. W subdomain:

Bash
1 v1.api.example.com/users

Implementacja wersjonowania w FastAPI

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from 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

Python
1 2 3 4 5 6 7 8 9 from 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ń

Python
1 2 3 4 5 6 7 8 9 from 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

Python
1 2 3 4 5 6 7 8 9 from 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:

Python
1 2 3 4 5 6 7 8 9 10 11 from 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

Bash
1 2 ✅ /users ❌ /getUsers

2. Nie przesadzaj z zagnieżdżeniami

Bash
1 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:

Bash
1 ✅ /orders?user_id=1

3. Wykorzystuj paginację

Bash
1 GET /api/users?page=1&limit=50

Implementacja:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from 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ów
  • total_pages — łączna liczba stron
  • has_next — czy jest następna strona
  • has_previous — czy jest poprzednia strona

5. Obsługuj błędy globalnie

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from 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

Python
1 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/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

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import 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:

Bash
1 pytest --maxfail=1 --disable-warnings -q

HTTPX dla asynchronicznych testów:

Python
1 2 3 4 5 6 7 8 import 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:

Python
1 2 3 def 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.