REST API w Pythonie: prosta aplikacja Flask

1
139
4/5 - (1 vote)

Nawigacja:

Czym jest REST API i dlaczego Python z Flask nadają się idealnie

REST API w kilku konkretnych zdaniach

REST API (Representational State Transfer API) to styl projektowania usług sieciowych, w którym komunikacja opiera się na protokole HTTP i zasobach reprezentowanych przez URL-e. Klient (np. frontend, aplikacja mobilna, inny serwer) wysyła żądania HTTP do serwera, a serwer zwraca odpowiedzi – najczęściej w formacie JSON.

Kluczowa idea REST: każda rzecz, którą chcesz obsłużyć, jest zasobem. Użytkownik, produkt, zadanie w to-do liście, artykuł w blogu – wszystko to zasoby, do których odwołujesz się przez adresy URL i manipulujesz nimi za pomocą standardowych metod HTTP.

Najczęściej używane metody HTTP w REST API:

  • GET – pobieranie danych (bez zmiany stanu na serwerze),
  • POST – tworzenie nowego zasobu,
  • PUT – pełna aktualizacja istniejącego zasobu,
  • PATCH – częściowa aktualizacja zasobu,
  • DELETE – usunięcie zasobu.

Standardowe, przewidywalne zachowanie sprawia, że klient nie musi poznawać „magii” po stronie serwera – wystarczy, że zna konwencje REST i strukturę API. Dlatego tworząc REST API w Pythonie z użyciem Flask, warto od początku trzymać się spójnego nazewnictwa i metod HTTP.

Dlaczego Python jest dobrym wyborem do REST API

Python jest jednym z najpopularniejszych języków w świecie web developmentu, data science i automatyzacji. To ma bezpośrednie przełożenie na tworzenie REST API – masz dostęp do ogromnego ekosystemu bibliotek, od prostych serwerów HTTP, przez ORM-y do baz danych, po narzędzia do testowania, bezpieczeństwa i dokumentacji.

Z praktycznej perspektywy Python jest czytelny i zwięzły. Kod API staje się łatwy do zrozumienia nawet dla osób, które nie są doświadczonymi programistami backendu. Dodatkowo Python dobrze integruje się z:

  • bazami danych (PostgreSQL, MySQL, SQLite, MongoDB),
  • systemami kolejek (Redis, RabbitMQ, Celery),
  • usługami chmurowymi (AWS, GCP, Azure),
  • narzędziami do uczenia maszynowego (idealne, gdy API ma wystawiać modele ML).

W codziennej pracy oznacza to, że REST API w Pythonie można szybko rozbudować o logikę biznesową, integracje i analitykę, nie przepisując połowy projektu. Dla małych i średnich aplikacji, w których liczy się czas dostarczenia rozwiązania, Python jest szczególnie wygodny.

Flask jako lekki framework pod REST API

Flask to microframework webowy w Pythonie. „Micro” oznacza, że sam w sobie dostarcza tylko niezbędne minimum: routing, obsługę żądań/odpowiedzi, szablony HTML (których w czystym REST API zwykle nie używamy) i prostą konfigurację. Wszystko ponad to dobierasz sam jako rozszerzenia lub własny kod.

Taki podejście idealnie pasuje do REST API:

  • tworzysz dokładnie to, co potrzebujesz, bez narzuconych struktur,
  • łatwiej zrozumieć, jak działa request → funkcja → response,
  • prototypy i małe serwisy powstają w godziny, a nie tygodnie,
  • architekturę od początku dopasowujesz do konkretnego projektu, a nie do wymagań frameworka.

Flask dobrze nadaje się zarówno do małych serwisów (np. jedno-endpointowe API do webhooków), jak i do średnich aplikacji o rozbudowanej logice, jeśli zadbasz o strukturę projektu. Przy bardzo dużych systemach bywa wygodniej sięgnąć po frameworki z większą ilością rozwiązań „z pudełka”, ale fundamenty REST API pozostają takie same.

Przygotowanie środowiska do tworzenia REST API we Flask

Instalacja Pythona i podstawowe narzędzia

Aby zacząć budować REST API w Pythonie przy użyciu Flask, potrzebne jest kilka elementów:

  1. Python w wersji 3.8 lub nowszej (stabilne i szeroko wspierane wydanie),
  2. menedżer pakietów pip, zwykle instalowany razem z Pythonem,
  3. edytor lub IDE: VS Code, PyCharm, a nawet zwykły edytor tekstu,
  4. narzędzie do testowania endpointów: curl, Postman, Insomnia lub rozszerzenie REST Client w VS Code.

Instalację Pythona najlepiej wykonać z oficjalnej strony lub menedżera pakietów systemu (np. apt, dnf, brew). Po instalacji możesz sprawdzić wersję poleceniem:

python --version
# lub w zależności od systemu
python3 --version

Wirtualne środowisko i izolacja zależności

Każdy projekt Pythonowy – zwłaszcza REST API rozwijane przez dłuższy czas – powinien mieć własne, izolowane środowisko. Dzięki temu różne projekty nie konfliktują między sobą wersjami bibliotek. Do tego służy virtualenv lub wbudowany moduł venv.

Przykładowa konfiguracja środowiska w katalogu projektu:

mkdir flask_rest_api
cd flask_rest_api

python -m venv venv

# Linux / macOS
source venv/bin/activate

# Windows (PowerShell)
.venvScriptsActivate.ps1

Po aktywacji środowiska terminal zwykle pokazuje prefiks (venv). Od tego momentu wszystkie instalowane pakiety trafią tylko do tego projektu. To szczególnie ważne, gdy tworzysz kilka API w różnych wersjach Flask lub innych bibliotek.

Instalacja Flask i podstawowych bibliotek

Główna zależność to oczywiście Flask. W praktycznej pracy przyda się też biblioteka do serializacji JSON (wbudowana w Pythona), narzędzia do automatycznego przeładowywania aplikacji w trakcie developmentu oraz ewentualnie narzędzie do zarządzania zmiennymi środowiskowymi.

Minimalna instalacja:

pip install Flask

Do wygodniejszej pracy w środowisku developerskim można dorzucić:

pip install python-dotenv

Dzięki python-dotenv ustawisz konfigurację w pliku .env, bez upychania haseł i kluczy w kodzie. Flask potrafi z niego korzystać automatycznie, jeśli odpowiednio ustawisz środowisko.

Okulary odbijające kod programu na ekranie monitora
Źródło: Pexels | Autor: Kevin Ku

Pierwsza prosta aplikacja Flask z REST API

Najprostszy szkielet aplikacji Flask

Podstawowy plik aplikacji REST API w Flask to najczęściej app.py. Minimalna wersja, która odpowiada na jedno żądanie GET, wygląda tak:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, Flask REST API!"

if __name__ == "__main__":
    app.run(debug=True)

Po uruchomieniu:

python app.py

serwer zacznie nasłuchiwać na http://127.0.0.1:5000/. Fragment debug=True włącza tryb developerski: automatyczny reload po zmianie kodu i proste debugowanie błędów. Tego parametru nie należy używać w środowisku produkcyjnym.

Prosta odpowiedź JSON zamiast tekstu

REST API zwykle nie odsyła surowego tekstu, tylko dane w formacie JSON. Flask udostępnia wygodną funkcję jsonify, która zamienia słowniki i listy Pythona w poprawny JSON, ustawiając odpowiedni nagłówek Content-Type: application/json.

Rozszerzmy minimalny przykład do prostego endpointu REST API:

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/status", methods=["GET"])
def status():
    data = {
        "status": "ok",
        "message": "REST API w Pythonie działa poprawnie",
    }
    return jsonify(data), 200

if __name__ == "__main__":
    app.run(debug=True)

Wywołanie:

curl http://127.0.0.1:5000/status

zwróci odpowiedź podobną do:

{"message":"REST API w Pythonie działa poprawnie","status":"ok"}

Zwracanie krotki (jsonify(data), 200) to czytelny sposób ustawiania kodu HTTP. Kod 200 oznacza powodzenie operacji. W kolejnych endpointach zacznie się pojawiać 201 (utworzono), 400 (błąd walidacji), 404 (nie znaleziono zasobu) i inne kody statusu.

Struktura katalogów nawet w prostym projekcie

Nawet tworząc prostą aplikację Flask z REST API, dobrze jest od razu zadbać o minimalny porządek w strukturze plików. Przykładowa, bardzo prosta struktura:

Sprawdź też ten artykuł:  Jak napisać własną grę w JavaScript?

flask_rest_api/
├─ venv/
├─ app.py
├─ models.py
├─ services.py
└─ requirements.txt

Nawet jeśli początkowo większość kodu znajdzie się w app.py, oddzielenie warstwy danych (models.py) i ewentualnej logiki biznesowej (services.py) ułatwi późniejszą rozbudowę API. W dużych projektach stosuje się bardziej rozbudowany podział, ale do nauki prostego REST API to w zupełności wystarcza.

Budowa prostego REST API we Flask krok po kroku

Model danych: lista zadań (to-do) w pamięci

Dobrym, praktycznym przykładem REST API w Pythonie z użyciem Flask jest prosta lista zadań (to-do). API będzie umożliwiało:

  • pobieranie listy wszystkich zadań,
  • pobieranie pojedynczego zadania,
  • dodawanie nowego zadania,
  • aktualizację istniejącego zadania,
  • usuwanie zadania.

Na początek przechowamy dane w zwykłej liście w pamięci. To dobre podejście do nauki logiki API, zanim dojdzie integracja z bazą danych. Przykładowy „model”:

tasks = [
    {
        "id": 1,
        "title": "Zainstalować Flask",
        "completed": False
    },
    {
        "id": 2,
        "title": "Stworzyć proste REST API w Pythonie",
        "completed": False
    }
]

W prawdziwej aplikacji dane trafią do bazy, ale operacje typu filtrowanie, wyszukiwanie czy walidacja będą wyglądały podobnie – tylko źródło danych będzie inne.

Endpoint GET: pobieranie listy zasobów

Pierwszy endpoint REST API zwróci listę wszystkich zadań. W Flask definiujesz go dekoratorem @app.route z metodą GET (domyślnie Flask pozwala na GET, więc można pominąć methods, ale lepiej być jawny w API).

from flask import Flask, jsonify

app = Flask(__name__)

tasks = [
    {"id": 1, "title": "Zainstalować Flask", "completed": False},
    {"id": 2, "title": "Stworzyć proste REST API w Pythonie", "completed": False},
]

@app.route("/tasks", methods=["GET"])
def get_tasks():
    return jsonify(tasks), 200

if __name__ == "__main__":
    app.run(debug=True)

Wywołanie:

curl http://127.0.0.1:5000/tasks

zwróci tablicę obiektów JSON. To typowy wzorzec: adres w liczbie mnogiej (/tasks) reprezentuje kolekcję zasobów.

Endpoint GET z parametrem ścieżki: pojedynczy zasób

REST API powinno umożliwić pobranie pojedynczego zadania na podstawie jego identyfikatora. W Flask parametr ścieżki definiujesz za pomocą składni <typ:nazwa>. Na przykład:

@app.route("/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id):
    task = next((t for t in tasks if t["id"] == task_id), None)
    if task is None:
        return jsonify({"error": "Task not found"}), 404
    return jsonify(task), 200

Dzieje się tu kilka istotnych rzeczy:

  • <int:task_id> mówi Flaskowi, że ten fragment URL ma być zamieniony na liczbę całkowitą i przekazany do funkcji jako task_id,
  • funkcja używa wyrażenia generatorowego i next(), aby znaleźć zadanie o danym ID,
  • w przypadku braku zasobu API zwraca kod 404 i prostą wiadomość błędu w JSON.

To prosty wzór na endpoint GET /zasoby/<id>, który można wielokrotnie wykorzystywać w kolejnych projektach. Wystarczy zmienić nazwę kolekcji i klucz identyfikatora.

Obsługa metod POST, PUT, PATCH i DELETE w Flask

Tworzenie zasobu: endpoint POST

Dodawanie nowego zadania wymaga endpointu POST na tej samej kolekcji, której używa GET. Konwencja REST: GET /tasks pobiera listę, a POST /tasks tworzy nowy element w tej kolekcji.

W Flask do odczytywania treści żądania (body) użyjesz obiektu request. Dla JSON-ów praktycznie zawsze stosuje się request.get_json().

Walidacja danych wejściowych w żądaniu POST

Zanim nowy zasób trafi do listy, trzeba sprawdzić podstawowe rzeczy: czy body jest poprawnym JSON-em, czy zawiera wymagane pola i czy mają odpowiednie typy. Bez tego API szybko zamieni się w generator trudnych do odtworzenia błędów.

Prosty przykład obsługi endpointu POST dla zasobów tasks z walidacją:

from flask import Flask, jsonify, request

app = Flask(__name__)

tasks = [
    {"id": 1, "title": "Zainstalować Flask", "completed": False},
    {"id": 2, "title": "Stworzyć proste REST API w Pythonie", "completed": False},
]

def get_next_id():
    if not tasks:
        return 1
    return max(task["id"] for task in tasks) + 1

@app.route("/tasks", methods=["POST"])
def create_task():
    data = request.get_json(silent=True)

    if data is None:
        return jsonify({"error": "Invalid JSON body"}), 400

    title = data.get("title")
    if not isinstance(title, str) or not title.strip():
        return jsonify({"error": "Field 'title' is required and must be a non-empty string"}), 400

    completed = data.get("completed", False)
    if not isinstance(completed, bool):
        return jsonify({"error": "Field 'completed' must be a boolean"}), 400

    task = {
        "id": get_next_id(),
        "title": title.strip(),
        "completed": completed,
    }
    tasks.append(task)

    return jsonify(task), 201

Kilka elementów usprawniających pracę:

  • request.get_json(silent=True) zwraca None zamiast rzucać wyjątek przy niepoprawnym JSON-ie,
  • prosta funkcja get_next_id() wylicza kolejne ID bez bazy danych,
  • kod 201 sygnalizuje, że zasób został utworzony.

W praktycznych projektach walidację często przenosi się do osobnej funkcji lub wykorzystuje biblioteki typu marshmallow czy pydantic, ale na start czysta walidacja w funkcji widoku jest w porządku.

Pełna aktualizacja zasobu: metoda PUT

Metoda PUT służy do pełnej aktualizacji zasobu. Konwencja: klient wysyła kompletną reprezentację obiektu, a serwer nadpisuje istniejący stan. Jeśli klient pominie jakieś pole, przy klasycznym PUT traktuje się je jak usunięte lub ustawione na domyślną wartość.

@app.route("/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
    task = next((t for t in tasks if t["id"] == task_id), None)
    if task is None:
        return jsonify({"error": "Task not found"}), 404

    data = request.get_json(silent=True)
    if data is None:
        return jsonify({"error": "Invalid JSON body"}), 400

    title = data.get("title")
    completed = data.get("completed")

    errors = {}
    if not isinstance(title, str) or not title.strip():
        errors["title"] = "Field 'title' is required and must be a non-empty string"
    if not isinstance(completed, bool):
        errors["completed"] = "Field 'completed' is required and must be a boolean"

    if errors:
        return jsonify({"errors": errors}), 400

    task["title"] = title.strip()
    task["completed"] = completed

    return jsonify(task), 200

PUT wymusza przesłanie kompletu danych. To czytelne, ale mało wygodne z poziomu niektórych frontendów, dlatego w wielu API równolegle dostępny jest PATCH.

Częściowa aktualizacja zasobu: metoda PATCH

PATCH służy do częściowej aktualizacji – klient wysyła tylko pola, które chce zmienić. Przy większych obiektach znacząco to zmniejsza rozmiar żądań i upraszcza logikę po stronie klienta.

@app.route("/tasks/<int:task_id>", methods=["PATCH"])
def patch_task(task_id):
    task = next((t for t in tasks if t["id"] == task_id), None)
    if task is None:
        return jsonify({"error": "Task not found"}), 404

    data = request.get_json(silent=True)
    if data is None:
        return jsonify({"error": "Invalid JSON body"}), 400

    allowed_fields = {"title", "completed"}
    unknown_fields = set(data.keys()) - allowed_fields
    if unknown_fields:
        return jsonify({"error": f"Unknown fields: {', '.join(sorted(unknown_fields))}"}), 400

    if "title" in data:
        title = data["title"]
        if not isinstance(title, str) or not title.strip():
            return jsonify({"error": "Field 'title' must be a non-empty string"}), 400
        task["title"] = title.strip()

    if "completed" in data:
        completed = data["completed"]
        if not isinstance(completed, bool):
            return jsonify({"error": "Field 'completed' must be a boolean"}), 400
        task["completed"] = completed

    return jsonify(task), 200

W prostych aplikacjach PUT i PATCH bywają używane zamiennie, ale rozdzielenie tych metod ułatwia budowanie spójnej dokumentacji API i kontrolowanie zachowania klientów.

Usuwanie zasobu: metoda DELETE

Usuwanie zadania to najkrótszy fragment logiki, ale i tutaj dobrze zachować konsekwencję w kodach odpowiedzi i formacie zwracanego JSON-a.

@app.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
    global tasks
    task = next((t for t in tasks if t["id"] == task_id), None)
    if task is None:
        return jsonify({"error": "Task not found"}), 404

    tasks = [t for t in tasks if t["id"] != task_id]

    return jsonify({"status": "deleted"}), 200

Alternatywnie można zwrócić kod 204 (No Content) bez body. W wielu projektach zwracanie prostego JSON-a (jak powyżej) jest wygodniejsze do logowania i debugowania klientów, dlatego 200 z krótką wiadomością bywa praktyczniejszy.

Spójne formaty błędów i odpowiedzi

Im wcześniej pojawi się w projekcie spójny format odpowiedzi, tym łatwiej dodawać kolejne endpointy i pisać klienta API. Zamiast zwracać różne struktury błędów, można oprzeć się na jednym schemacie.

Najprostszy wspólny format:

  • dla powodzenia: {"data": ...} lub „gołe” dane (jak obecnie) – ważne, by to ujednolicić,
  • dla błędów: obiekt z polem error (prosty string) lub errors (słownik pól → opis błędu).

Przykładowa ujednolicona odpowiedź błędu mogłaby wyglądać tak:

{
  "error": {
    "message": "Validation failed",
    "details": {
      "title": "Field 'title' is required"
    }
  }
}

Jeśli taki format zostanie ustalony na początku, można później zbudować pomocniczą funkcję, która tworzy odpowiedzi błędów i wywoływać ją w wielu miejscach zamiast ręcznie składać słowniki.

Globalna obsługa błędów w Flask

W miarę rozwoju API ręczne obsługiwanie wszystkich wyjątków staje się niewygodne. Flask pozwala podpiąć globalne handlery dla konkretnych typów błędów oraz kodów HTTP.

Prosty przykład obsługi błędu 404 i ogólnego wyjątku:

Sprawdź też ten artykuł:  Czym różni się backend od frontendu? Wyjaśnienie dla laików

from werkzeug.exceptions import HTTPException

@app.errorhandler(404)
def handle_404(error):
    return jsonify({"error": "Resource not found"}), 404

@app.errorhandler(Exception)
def handle_exception(error):
    if isinstance(error, HTTPException):
        return jsonify({"error": error.description}), error.code

    # Niespodziewany błąd - nie zdradzaj szczegółów implementacyjnych
    return jsonify({"error": "Internal server error"}), 500

Dzięki temu nie trzeba w każdym widoku powtarzać tych samych konstrukcji; rzucenie abort(404) przejdzie przez handler i wygeneruje JSON w spójnym formacie.

Rozdzielenie logiki na pliki: app.py, models.py, services.py

Gdy endpointów przybywa, izolowanie warstw kodu szybko zaczyna procentować. Minimalny podział na trzy pliki pozwala utrzymać porządek.

Przykładowy models.py dla prostej listy zadań w pamięci:

# models.py

tasks = [
    {"id": 1, "title": "Zainstalować Flask", "completed": False},
    {"id": 2, "title": "Stworzyć proste REST API w Pythonie", "completed": False},
]

def get_next_task_id():
    if not tasks:
        return 1
    return max(task["id"] for task in tasks) + 1

Plik services.py zawierający logikę biznesową:

# services.py

from typing import Optional, Dict, List
from .models import tasks, get_next_task_id

def list_tasks() -> List[dict]:
    return tasks

def find_task(task_id: int) -> Optional[dict]:
    return next((t for t in tasks if t["id"] == task_id), None)

def create_task(title: str, completed: bool = False) -> dict:
    task = {
        "id": get_next_task_id(),
        "title": title.strip(),
        "completed": completed,
    }
    tasks.append(task)
    return task

def update_task_full(task_id: int, title: str, completed: bool) -> Optional[dict]:
    task = find_task(task_id)
    if task is None:
        return None
    task["title"] = title.strip()
    task["completed"] = completed
    return task

def update_task_partial(task_id: int, fields: Dict) -> Optional[dict]:
    task = find_task(task_id)
    if task is None:
        return None

    if "title" in fields:
        task["title"] = fields["title"].strip()
    if "completed" in fields:
        task["completed"] = fields["completed"]
    return task

def delete_task(task_id: int) -> bool:
    global tasks
    task = find_task(task_id)
    if task is None:
        return False
    tasks = [t for t in tasks if t["id"] != task_id]
    return True

Odpowiednio uproszczony app.py:

# app.py

from flask import Flask, jsonify, request
from services import (
    list_tasks,
    find_task,
    create_task,
    update_task_full,
    update_task_partial,
    delete_task,
)

app = Flask(__name__)

@app.route("/tasks", methods=["GET"])
def get_tasks():
    return jsonify(list_tasks()), 200

@app.route("/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id):
    task = find_task(task_id)
    if task is None:
        return jsonify({"error": "Task not found"}), 404
    return jsonify(task), 200

@app.route("/tasks", methods=["POST"])
def create_task_endpoint():
    data = request.get_json(silent=True)
    if data is None or not isinstance(data.get("title"), str):
        return jsonify({"error": "Field 'title' is required"}), 400

    completed = data.get("completed", False)
    if not isinstance(completed, bool):
        return jsonify({"error": "Field 'completed' must be a boolean"}), 400

    task = create_task(data["title"], completed)
    return jsonify(task), 201

# ... analogicznie można przepiąć PUT, PATCH, DELETE na warstwę services

if __name__ == "__main__":
    app.run(debug=True)

Taki podział nie jest jeszcze zaawansowaną architekturą, ale pozwala spokojnie rozbudowywać aplikację bez efektu „jednego gigantycznego pliku app.py”.

Blueprinty Flask: modularne grupowanie endpointów

W większych aplikacjach grupowanie endpointów w blueprinty znacząco ułatwia utrzymanie kodu i ponowne wykorzystanie fragmentów API. Każdy blueprint może obsługiwać jedną domenę biznesową (np. zadania, użytkownicy, raporty).

Przykładowy blueprint dla modułu tasks:

# tasks_views.py

from flask import Blueprint, jsonify, request
from services import (
    list_tasks,
    find_task,
    create_task,
    update_task_full,
    update_task_partial,
    delete_task,
)

tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks")

@tasks_bp.get("")
def get_tasks():
    return jsonify(list_tasks()), 200

@tasks_bp.get("/<int:task_id>")
def get_task(task_id):
    task = find_task(task_id)
    if task is None:
        return jsonify({"error": "Task not found"}), 404
    return jsonify(task), 200

@tasks_bp.post("")
def create_task_endpoint():
    data = request.get_json(silent=True)
    if data is None or not isinstance(data.get("title"), str):
        return jsonify({"error": "Field 'title' is required"}), 400

    completed = data.get("completed", False)
    if not isinstance(completed, bool):
        return jsonify({"error": "Field 'completed' must be a boolean"}), 400

    task = create_task(data["title"], completed)
    return jsonify(task), 201

Rejestracja blueprintu w głównej aplikacji:

# app.py

from flask import Flask
from tasks_views import tasks_bp

app = Flask(__name__)
app.register_blueprint(tasks_bp)

if __name__ == "__main__":
    app.run(debug=True)

Przy dodaniu kolejnych modułów (np. users_bp, auth_bp) wystarczy je zarejestrować w jednym miejscu. To typowy sposób organizowania większych aplikacji RESTowych opartych na Flasku.

Filtrowanie, paginacja i parametry zapytań

Proste endpointy GET szybko zaczynają potrzebować dodatkowych parametrów: filtrowania po stanie, paginacji, sortowania. Flask udostępnia do tego obiekt request.args, który reprezentuje parametry query string.

Rozszerzona lista zadań z filtrowaniem po polu completed i prostą paginacją:

@app.route("/tasks", methods=["GET"])
def get_tasks():
    # query string: /tasks?completed=true&page=1&page_size=10
    completed_param = request.args.get("completed")
    page = request.args.get("page", default=1, type=int)
    page_size = request.args.get("page_size", default=10, type=int)

    filtered_tasks = tasks

    if completed_param is not None:
        if completed_param.lower() in ("true", "1"):
            filtered_tasks = [t for t in tasks if t["completed"] is True]
        elif completed_param.

Zaawansowane filtrowanie i paginacja w warstwie usług

Fragment z filtrowaniem w widoku można przenieść do warstwy usług, dzięki czemu logika biznesowa nie będzie zmieszana z obsługą HTTP. Przykładowa rozbudowa services.py:

# services.py (dalszy fragment)

from typing import Optional, Dict, List, Tuple

def list_tasks_filtered(
    completed: Optional[bool] = None,
    page: int = 1,
    page_size: int = 10,
) -> Tuple[List[dict], int]:
    """
    Zwraca listę zadań po filtrowaniu + łączną liczbę rekordów przed paginacją.
    """
    filtered = tasks

    if completed is not None:
        filtered = [t for t in filtered if t["completed"] is completed]

    total = len(filtered)

    # Prosta paginacja w pamięci
    if page < 1:
        page = 1
    if page_size < 1:
        page_size = 10

    start = (page - 1) * page_size
    end = start + page_size
    paginated = filtered[start:end]

    return paginated, total

Widok w Flasku staje się wtedy cieńszy i skupiony tylko na tłumaczeniu HTTP ↔ Python:

@app.route("/tasks", methods=["GET"])
def get_tasks():
    # /tasks?completed=true&page=1&page_size=10
    completed_param = request.args.get("completed")
    page = request.args.get("page", default=1, type=int)
    page_size = request.args.get("page_size", default=10, type=int)

    completed_value = None
    if completed_param is not None:
        if completed_param.lower() in ("true", "1"):
            completed_value = True
        elif completed_param.lower() in ("false", "0"):
            completed_value = False
        else:
            return jsonify({"error": "Invalid 'completed' value"}), 400

    items, total = list_tasks_filtered(
        completed=completed_value,
        page=page,
        page_size=page_size,
    )

    return jsonify(
        {
            "items": items,
            "total": total,
            "page": page,
            "page_size": page_size,
        }
    ), 200

Klient dostaje nie tylko listę rekordów, ale też informację o liczbie wszystkich elementów – przydaje się to np. w interfejsach webowych z paginacją.

Walidacja danych wejściowych bez dodatkowych bibliotek

Przy jednym prostym polu typu title wystarcza szybkie if, natomiast przy większej liczbie pól i powtarzalnych reguł przydaje się mała warstwa walidacji. Można zacząć od najprostszej funkcji, bez sięgania po zewnętrzne biblioteki.

# validators.py

from typing import Tuple, Dict, Any

def validate_task_payload(data: Dict[str, Any], partial: bool = False) -> Tuple[bool, Dict]:
    """
    Zwraca (is_valid, errors).
    partial=True - dla PATCH (wszystkie pola opcjonalne),
    partial=False - dla POST/PUT (pola wymagane).
    """
    errors = {}

    if data is None:
        return False, {"_general": "JSON body is required"}

    if not partial:
        if "title" not in data:
            errors["title"] = "Field 'title' is required"

    if "title" in data:
        if not isinstance(data["title"], str):
            errors["title"] = "Field 'title' must be a string"
        elif not data["title"].strip():
            errors["title"] = "Field 'title' cannot be empty"

    if "completed" in data:
        if not isinstance(data["completed"], bool):
            errors["completed"] = "Field 'completed' must be a boolean"

    return len(errors) == 0, errors

Wykorzystanie w widokach:

# app.py (fragment)

from validators import validate_task_payload

@app.route("/tasks", methods=["POST"])
def create_task_endpoint():
    data = request.get_json(silent=True)
    is_valid, errors = validate_task_payload(data, partial=False)
    if not is_valid:
        return jsonify({"error": "Validation failed", "details": errors}), 400

    task = create_task(
        title=data["title"],
        completed=data.get("completed", False),
    )
    return jsonify(task), 201

@app.route("/tasks/<int:task_id>", methods=["PATCH"])
def patch_task_endpoint(task_id):
    data = request.get_json(silent=True)
    is_valid, errors = validate_task_payload(data, partial=True)
    if not is_valid:
        return jsonify({"error": "Validation failed", "details": errors}), 400

    task = update_task_partial(task_id, data)
    if task is None:
        return jsonify({"error": "Task not found"}), 404
    return jsonify(task), 200

Taki prosty walidator można rozbudowywać (np. długość tytułu, dozwolone znaki) bez dotykania samych endpointów.

Konfiguracja środowisk: development, test, production

Jedna z pierwszych przeszkód przy przenoszeniu aplikacji z lokalnego komputera na serwer to konfiguracja. Flask umożliwia ładowanie ustawień z plików czy zmiennych środowisk.

Podstawowy wzorzec z klasami konfiguracji:

# config.py

import os

class BaseConfig:
    DEBUG = False
    TESTING = False
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key")

class DevelopmentConfig(BaseConfig):
    DEBUG = True

class TestingConfig(BaseConfig):
    TESTING = True

class ProductionConfig(BaseConfig):
    # Można tu dodać np. konfigurację bazy, logowania itp.
    pass

config_map = {
    "development": DevelopmentConfig,
    "testing": TestingConfig,
    "production": ProductionConfig,
}

Fabryka aplikacji, która wybiera konfigurację na podstawie zmiennej środowiskowej:

# app_factory.py

import os
from flask import Flask
from config import config_map
from tasks_views import tasks_bp

def create_app() -> Flask:
    env = os.environ.get("FLASK_ENV", "development")
    config_class = config_map.get(env, config_map["development"])

    app = Flask(__name__)
    app.config.from_object(config_class)

    app.register_blueprint(tasks_bp)

    # Tu można zarejestrować handlery błędów, rozszerzenia, itp.
    return app

Uruchomienie dla środowiska developerskiego:

export FLASK_ENV=development
flask --app app_factory:create_app run

Aby przełączyć się na konfigurację produkcyjną, wystarczy zmienić FLASK_ENV bez modyfikacji kodu.

Testowanie REST API w Flasku

API bez testów bardzo łatwo „zepsuć” przy drobnej refaktoryzacji. Flask ma wbudowany mechanizm klienta testowego, który pozwala wywoływać endpointy bez uruchamiania serwera HTTP.

# test_tasks.py

import json
from app_factory import create_app

def setup_app():
    app = create_app()
    app.config["TESTING"] = True
    return app

def test_create_task_success():
    app = setup_app()
    client = app.test_client()

    response = client.post(
        "/tasks",
        data=json.dumps({"title": "Nowe zadanie"}),
        content_type="application/json",
    )

    assert response.status_code == 201
    data = response.get_json()
    assert data["title"] == "Nowe zadanie"
    assert data["completed"] is False

def test_create_task_validation_error():
    app = setup_app()
    client = app.test_client()

    response = client.post(
        "/tasks",
        data=json.dumps({"completed": True}),
        content_type="application/json",
    )

    assert response.status_code == 400
    data = response.get_json()
    assert data["error"] == "Validation failed"
    assert "title" in data["details"]

W praktyce testy uruchamia się przy użyciu narzędzi takich jak pytest, ale sama idea pozostaje taka sama: tworzenie aplikacji w pamięci i wykonywanie żądań HTTP po stronie Pythona.

Logowanie i debugowanie błędów

Komunikat {"error": "Internal server error"} jest poprawny dla klienta, ale mało użyteczny podczas diagnozowania problemu. W środku funkcji obsługujących wyjątki można dodać logowanie.

# logging_config.py

import logging
import sys

def configure_logging():
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)

    handler = logging.StreamHandler(sys.stdout)
    formatter = logging.Formatter(
        "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"
    )
    handler.setFormatter(formatter)
    logger.addHandler(handler)

Rejestracja logowania i użycie we wspólnym handlerze błędów:

# app_factory.py (fragment)

from werkzeug.exceptions import HTTPException
from flask import jsonify
from logging_config import configure_logging
import logging

def create_app() -> Flask:
    configure_logging()
    app = Flask(__name__)
    # ...

    @app.errorhandler(Exception)
    def handle_exception(error):
        if isinstance(error, HTTPException):
            return jsonify({"error": error.description}), error.code

        logging.exception("Unexpected error occurred")
        return jsonify({"error": "Internal server error"}), 500

    return app

W środowisku produkcyjnym logi najczęściej trafiają do systemu typu journald, ELK lub innego agregatora logów. Już samo dodanie logging.exception potrafi zaoszczędzić wiele godzin na „zgadywanie”, co poszło nie tak.

Proste uwierzytelnianie tokenem

Nawet bardzo małe API zwykle musi w pewnym momencie ograniczyć dostęp tylko do uprawnionych klientów. Dla prostych zastosowań można użyć stałego tokena przekazywanego w nagłówku Authorization.

# auth.py

from flask import request, jsonify, current_app
from functools import wraps

def require_api_token(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        token = request.headers.get("Authorization")
        expected = current_app.config.get("API_TOKEN")

        if not expected or token != f"Bearer {expected}":
            return jsonify({"error": "Unauthorized"}), 401

        return f(*args, **kwargs)

    return wrapper

Zabezpieczenie wybranych endpointów:

# tasks_views.py (fragment)

from auth import require_api_token

@tasks_bp.post("")
@require_api_token
def create_task_endpoint():
    data = request.get_json(silent=True)
    # ... reszta jak wcześniej

Konfiguracja sekretnego tokena np. w ProductionConfig:

# config.py (fragment)

class ProductionConfig(BaseConfig):
    API_TOKEN = os.environ.get("API_TOKEN")

To nie jest pełne bezpieczeństwo z prawdziwą obsługą użytkowników, ale jako pierwszy krok do ograniczenia API dla świata zewnętrznego sprawdza się zaskakująco często, np. przy wewnętrznych integracjach między systemami.

Połączenie z bazą danych przy użyciu SQLAlchemy

Trzymanie danych w pamięci szybko przestaje wystarczać. Najczęstszy następny krok to baza SQL (np. SQLite lokalnie, PostgreSQL na produkcji) i ORM, taki jak SQLAlchemy.

# extensions.py

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

Model zadania zapisany w bazie:

# models_db.py

from datetime import datetime
from extensions import db

class Task(db.Model):
    __tablename__ = "tasks"

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(255), nullable=False)
    completed = db.Column(db.Boolean, default=False, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

    def to_dict(self):
        return {
            "id": self.id,
            "title": self.title,
            "completed": self.completed,
            "created_at": self.created_at.isoformat() + "Z",
        }

Inicjalizacja bazy w fabryce aplikacji:

# app_factory.py (fragment)

from extensions import db

def create_app() -> Flask:
    # ...
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///tasks.db"
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

    db.init_app(app)

    with app.app_context():
        db.create_all()

    app.register_blueprint(tasks_bp)
    return app

Warstwa usług korzystająca z modelu ORM:

# services_db.py

from typing import Optional, Dict, List, Tuple
from extensions import db
from models_db import Task

def list_tasks_db() -> List[dict]:
    return [t.to_dict() for t in Task.query.all()]

def list_tasks_filtered_db(
    completed: Optional[bool] = None,
    page: int = 1,
    page_size: int = 10,
) -> Tuple[List[dict], int]:
    query = Task.query

    if completed is not None:
        query = query.filter(Task.completed.is_(completed))

    total = query.count()

    items = (
        query
        .order_by(Task.id)
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all()
    )

    return [t.to_dict() for t in items], total

def create_task_db(title: str, completed: bool = False) -> dict:
    task = Task(title=title.strip(), completed=completed)
    db.session.add(task)
    db.session.commit()
    return task.to_dict()

def find_task_db(task_id: int) -> Optional[Task]:
    return Task.query.get(task_id)

def update_task_full_db(task_id: int, title: str, completed: bool) -> Optional[dict]:
    task = find_task_db(task_id)
    if task is None:
        return None
    task.title = title.strip()
    task.completed = completed
    db.session.commit()
    return task.to_dict()

def update_task_partial_db(task_id: int, fields: Dict) -> Optional[dict]:
    task = find_task_db(task_id)
    if task is None:
        return None
    if "title" in fields:
        task.title = fields["title"].strip()
    if "completed" in fields:
        task.completed = fields["completed"]
    db.session.commit()
    return task.to_dict()

def delete_task_db(task_id: int) -> bool:
    task = find_task_db(task_id)
    if task is None:
        return False
    db.session.delete(task)
    db.session.commit()
    return True

Po podmianie wywołań z services.py na odpowiedniki z services_db.py API zachowuje ten sam kontrakt HTTP, ale dane przestają znikać po restarcie aplikacji.

Wersjonowanie API

Przy każdej poważniejszej zmianie kontraktu (np. inne pola w odpowiedziach) dobrze jest zacząć wersjonować endpointy. Najczęściej stosuje się wersję w URL, np. /api/v1/tasks.

Blueprinty bardzo ułatwiają ten wzorzec:

# tasks_v1_views.py

from flask import Blueprint, jsonify, request
from services_db import (
list_tasks_filtered_db,
create_task_db,
find_task_db,
update_task_full_db,
update_task_partial_db,
delete_task_db,
)
from validators import validate_task_payload

tasks_v1_bp = Blueprint("tasks_v1", __name__, url_prefix="/api/v1/tasks")

@tasks_v1_bp.get("")
def get_tasks_v1():
completed_param = request.args.get("completed")
page = request.args.get("page", default=1, type=int)
page_size = request.args.get("page_size", default=10, type=int)

completed_value = None
if completed_param is not None:
if completed_param.lower() in ("true", "1"):
completed_value = True
elif completed_param.

Najczęściej zadawane pytania (FAQ)

Co to jest REST API w praktyce i do czego mogę je wykorzystać?

REST API to sposób tworzenia usług sieciowych, w którym komunikacja odbywa się przez HTTP, a wszystko jest traktowane jako zasób dostępny pod konkretnym adresem URL. Klient (np. frontend, aplikacja mobilna, inny serwer) wysyła żądania HTTP, a serwer zwraca odpowiedzi – zazwyczaj w formacie JSON.

REST API wykorzystasz m.in. do:

  • budowy backendu pod aplikację webową lub mobilną,
  • integracji między serwisami (np. Twój system i zewnętrzne usługi SaaS),
  • wystawiania modeli ML lub systemów analitycznych przez HTTP.

Dlaczego Python jest dobrym wyborem do tworzenia REST API?

Python jest prosty w nauce, czytelny i ma ogromny ekosystem bibliotek, co przyspiesza tworzenie REST API. Łatwo zintegrować go z bazami danych (PostgreSQL, MySQL, SQLite, MongoDB), kolejkami (Redis, RabbitMQ), chmurą (AWS, GCP, Azure) i narzędziami ML.

Dzięki temu backend w Pythonie da się szybko rozbudować o dodatkową logikę biznesową i integracje, bez przepisywania całej aplikacji. To szczególnie ważne w małych i średnich projektach, gdzie liczy się czas dostarczenia rozwiązania.

Czym Flask różni się od innych frameworków do REST API w Pythonie?

Flask to tzw. microframework – dostarcza tylko podstawy (routing, obsługa żądań/odpowiedzi, prosta konfiguracja), a całą resztę dobierasz sam jako rozszerzenia lub własny kod. Dzięki temu nie narzuca z góry rozbudowanej struktury projektu.

W praktyce oznacza to:

  • szybkie prototypowanie prostych API (nawet w kilka godzin),
  • łatwe zrozumienie przepływu request → funkcja → response,
  • możliwość dopasowania architektury do potrzeb projektu, a nie do wymagań frameworka.

Flask świetnie sprawdza się w małych i średnich serwisach; przy bardzo dużych systemach częściej wybiera się frameworki z większą ilością funkcji „z pudełka”.

Jak zacząć tworzyć REST API we Flask – jakie są pierwsze kroki?

Na start potrzebujesz zainstalowanego Pythona (min. 3.8), menedżera pakietów pip i edytora kodu (np. VS Code, PyCharm). Następnie:

  • tworzysz katalog projektu i wirtualne środowisko: python -m venv venv,
  • aktywujesz środowisko (source venv/bin/activate lub .venvScriptsActivate.ps1 na Windows),
  • instalujesz Flask: pip install Flask.

Potem dodajesz plik app.py z prostą aplikacją Flask i uruchamiasz serwer komendą python app.py.

Jak zrobić prosty endpoint REST API we Flask, który zwraca JSON?

Do zwracania JSON-a Flask udostępnia funkcję jsonify, która zamienia słownik Pythona na poprawny JSON i ustawia odpowiedni nagłówek. Przykładowy endpoint:


from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/status", methods=["GET"])
def status():
    data = {"status": "ok", "message": "REST API w Pythonie działa poprawnie"}
    return jsonify(data), 200

Po uruchomieniu aplikacji możesz wywołać curl http://127.0.0.1:5000/status, a serwer zwróci odpowiedź JSON z kodem HTTP 200.

Jakie metody HTTP powinienem znać przy tworzeniu REST API?

Najważniejsze metody HTTP używane w REST API to:

  • GET – pobieranie danych bez zmiany stanu na serwerze,
  • POST – tworzenie nowego zasobu,
  • PUT – pełna aktualizacja istniejącego zasobu,
  • PATCH – częściowa aktualizacja istniejącego zasobu,
  • DELETE – usuwanie zasobu.

Trzymając się tych konwencji, tworzysz API przewidywalne dla klientów i łatwe w integracji z innymi systemami.

Jak powinna wyglądać podstawowa struktura katalogów w małym projekcie Flask API?

Nawet w prostym REST API warto od razu zadbać o minimalny porządek. Przykładowa struktura:


flask_rest_api/
├─ venv/
├─ app.py
├─ models.py
├─ services.py
└─ requirements.txt

app.py zawiera główną aplikację Flask i definicje endpointów, models.py – warstwę danych (np. modele powiązane z bazą), a services.py – logikę biznesową. Dzięki temu kod jest czytelniejszy i łatwiejszy do rozwijania w miarę rozbudowy API.

Wnioski w skrócie

  • REST API opiera się na zasobach identyfikowanych przez URL-e oraz standardowe metody HTTP (GET, POST, PUT, PATCH, DELETE), co zapewnia przewidywalne i spójne zachowanie po stronie klienta i serwera.
  • W podejściu REST „wszystko jest zasobem” – użytkownicy, produkty, zadania czy artykuły są obsługiwane przez odpowiednie adresy URL i manipulowane za pomocą właściwych metod HTTP.
  • Python jest szczególnie dobry do tworzenia REST API dzięki czytelnej składni oraz bogatemu ekosystemowi bibliotek do baz danych, kolejek, chmury i uczenia maszynowego, co ułatwia szybkie rozbudowywanie logiki biznesowej.
  • Flask jako lekki microframework dostarcza tylko podstawy (routing, obsługa żądań i odpowiedzi), pozwalając programiście samodzielnie dobrać resztę elementów i dopasować architekturę dokładnie do potrzeb projektu.
  • Flask świetnie sprawdza się w małych i średnich aplikacjach oraz prototypach REST API, umożliwiając stworzenie działającego serwisu w krótkim czasie, przy zachowaniu przejrzystego przepływu request → funkcja → response.
  • Każde REST API w Pythonie powinno działać w odizolowanym wirtualnym środowisku (venv), aby uniknąć konfliktów wersji bibliotek między różnymi projektami.
  • Podstawowy start pracy z REST API we Flask wymaga instalacji Pythona 3.8+, utworzenia i aktywacji wirtualnego środowiska oraz instalacji Flask (opcjonalnie python-dotenv do wygodnego zarządzania konfiguracją w pliku .env).

1 KOMENTARZ

  1. Bardzo ciekawy artykuł! Bardzo doceniam klarowne i przystępne wyjaśnienie, jak stworzyć prostą aplikację REST API w Pythonie przy użyciu frameworka Flask. Dzięki przejrzystym przykładom i krok po kroku instrukcjom nawet początkujący programiści powinni bez problemu zrozumieć omawiane zagadnienia.

    Jednakże, jako doświadczony programista, chciałbym zobaczyć więcej zaawansowanych technik i praktyk dotyczących tworzenia REST API w Pythonie. Może rozważyliby Państwo dodanie sekcji z bardziej zaawansowanymi przykładami lub omówieniem różnych sposobów autoryzacji i uwierzytelniania w REST API w Pythonie. To mogłoby uzupełnić artykuł i uczynić go jeszcze bardziej wartościowym dla czytelników o większym doświadczeniu w temacie.

    Mimo tej małej uwagi, ogólnie jestem zadowolony z treści artykułu i chętnie korzystam z takich praktycznych porad w swojej pracy. Dziękuję za podzielenie się tą wiedzą!

Komentowanie jest ograniczone do zalogowanych użytkowników.