Bei meinem aktuellen Kundenprojekt wird mit Python viel Data-Engineering betrieben. Um die Bedienung zu vereinfachen und gleichzeitig Schnittstellen für Umsysteme anzubieten, suchten wir nach einem geeigneten Web-Framework und fanden “FastAPI”. FastAPI ist ein modernes Web-Framework für die Entwicklung von APIs mit Python. Es zeichnet sich durch seine Geschwindigkeit, einfache Handhabung und eine starke Integration mit Typisierung in Python aus. In diesem Blog stelle ich das Framework anhand einer kleinen Demo-Applikation kurz vor.
Warum FastAPI?
FastAPI zeichnet sich durch seine hohe Leistung, intuitive Syntax und automatische Generierung von OpenAPI-Dokumentationen aus. OpenAPI ist vielen wohl eher unter dem Namen “Swagger” bekannt – jedenfalls ist das sehr praktisch und wir werden später noch auf das Thema zurückkommen. FastAPI basiert auf Starlette (für das Web-Framework) und Pydantic (für die Datenvalidierung). Darüber hinaus ist FastAPI von Grund auf asynchron und nutzt den async-await Syntax, was sich besonders positiv auf die Performance und die Handhabung von I/O-gebundenen Aufgaben auswirkt. Das Projekt ist ausserdem Open-Source und wird von einer beachtlich grossen Community aktiv gepflegt und weiterentwickelt (siehe: https://github.com/tiangolo/fastapi).
Schnellstart mit FastAPI
Ist Phyton v3.8 oder neuer auf dem System verfügbar, erstellen wir zunächst eine Phython-Umgebung und installieren anschliessend die nötigen Abhängigkeiten:
> python –m venv .venv > .\.venv\Script\actiavte (.venv)> pip install fastapi==0.111.0
Damit sind wir bereits startklar und erstellen nun eine erste Datei main.py
. Diese dient als Startpunkt der Applikation und folgender Code reicht, um eine sehr simple Web-API zu haben:
# main.py from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q}
Dieser Code erstellt eine FastAPI-Anwendung mit zwei Endpunkten. Der erste Endpunkt reagiert auf die URL «/» und gibt ein statisches Dictionary als JSON zurück. Der zweite Endpunkt reagiert auf die URL «items/xxx» und erwartet eine Ganzzahl als URL-Parameter sowie einem optionalen Parameter q
.
Wir starten die Anwendung via Konsole:
> uvicorn main:app --reload
Et voilà, der Browser zeigt auf dem FastAPI-Port 8000 das “Hello World” JSON an 😃.
Und wie war das nun mit der OpenAPI aka. Swagger-Dokumentation? Die Dokumentation wird beim Starten automatisch erstellt und steht unter der URL «/docs
» zur Verfügung (ganze URL: http://localhost:8000/docs). Die gesamte API-Dokumentation widerspiegelt somit immer den aktuell Codestand und kommt ganz ohne manuelles Zutun mit:
Hier wird FastAPI seinem Namen schon mal gerecht. Innert kürzester Zeit steht eine einfache REST-API inklusive Dokumentation👌.
Datenvalidierung und Serialisierung mit Pydantic
Bei REST-Schnittstellen wird eigentlich immer JSON als Format gewählt – zumindest entspricht dies meiner Erfahrung. Python und JSON sind zum Glück gute Freunde – sind also sehr gut kompatibel und lassen sich leicht miteinander verwenden – und da gesellt sich auch FastAPI munter in die Runde. FastAPI basiert auf Pydantic, eine Python-Bibliothek für Parsing und Validierung. Es ermöglicht die Definition von Datenmodellen (alias Klassen) mit strenger Typprüfung, was die Sicherheit, Zuverlässigkeit und Lesbarkeit des Codes deutlich verbessert.
Im Code kann das dann etwa so aussehen:
# model.py from pydantic import BaseModel class ShopItem(BaseModel): name: str description: str | None = None price: int tax: float | None = None
Dieses Modell kann ich nun direkt als Parameter für einen neuen Endpunkt einsetzten. So wird es automatisch validiert sowie de- und serialisiert:
# main.py from fastapi import FastAPI from model import ShopItem app = FastAPI() @app.post("/items") def create_item(item: ShopItem): return item
Einen erneuten Blick auf die Dokumentationsseite zeigt uns den neuen Endpunkt mit einem Beispiel des geforderten Datenmodells.
Enthält eine Anfrage nun ungültige Werte (also z.B. ein Array
anstelle string
für das Feld name
), wird dies vom Framework erkannt und direkt mit einem ausführlichen Fehler beantwortet. Wer also keine Lust auf manuelles Parsen, Validieren und De-/Serialisieren kann das zuversichtlich dem Framework überlassen.
Endpunkte erweitern mit «Depends»
Ein besonders mächtiges Feature von FastAPI ist die Depends
Funktionalität. Mit Depends
können Abhängigkeiten in den Routen deklarativ definiert werden, was für sauberen und modularisierten Code dienlich sein kann. Dieses Prinzip kennt man auch aus höheren Programmiersprachen und ist allgemein unter dem Begriff “Dependency Injection” bekannt. Diese Funktionalität ermöglicht es jedenfalls, wiederverwendbare Komponenten zu erstellen, die in verschiedenen Routen eingesetzt werden können, ohne den Code zu duplizieren. Zum Beispiel können Datenbankverbindungen, Authentifizierungsmechanismen oder andere gemeinsame Ressourcen als Abhängigkeiten definiert werden. Dies vereinfacht die Wartung und fördert die Wiederverwendbarkeit von Code. Sehr häufig wird Depends für die Datenbankverbindung verwendet, wo eine Generatormethode eine Datenbank-Session zur Verfügung stellt. Dazu findet man im Netz sehr viele Beispiele und ich verzichte an dieser Stelle auf das Datenbank-Szenario, zeige dafür ein Beispiel zum Auslesen eines Cookies:
# main.py from http import HTTPStatus from fastapi import Cookie, Depends, FastAPI, HTTPException from fastapi.responses import JSONResponse app = FastAPI() def get_cookie(username: str | None = Cookie(None)) -> str: if username is None: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Cookie not found") return username @app.get("/cookie") def read_cookie(username: str | None = Depends(get_cookie)) -> JSONResponse: return {"user": username} @app.post("/cookie") def create_cookie() -> JSONResponse: content = {"message": "Join us @Noser, we have cookies =)"} response = JSONResponse(content=content) response.set_cookie(key="username", value="joel_geiser") return response
Auf dem POST-Endpunkt «/cookie» wird der Antwort zum Browser ein Cookie mitgegeben. Das Cookie hat den Schlüssel username
und als Wert meinen Namen. Wird nun eine GET-Anfrage auf «/cookie» gestellt, wird die get_cookie()
Funktion aufgerufen und dort das Cookie mit dem Schlüssel username
abgefragt. Wird das Cookie nicht gefunden, wird ein HTTP–Fehler geworfen, ansonsten wird der Wert dargestellt.
Testen mit pytest
Während, oder spätestens nach, der Implementierung der Web-API, ploppt aus dem Entwicklerherz gerne mal ein “Läuft, aber wie teste ich das🤔?” auf. Denn das Testen von Endpunkten ist unerlässlich, um die Stabilität und Korrektheit der Anwendung sicherzustellen. Mit pytest
und dem TestClient
von FastAPI kann man leicht automatisierte Tests für seine API schreiben. TestClient
basiert auf requests
und bietet eine einfache Möglichkeit, HTTP-Anfragen an die FastAPI-Applikation zu stellen und die Antworten zu überprüfen.
Als kleine Voraussetzung ist die Installation von pytest
sowie httpx
erforderlich:
(.venv)> pip install pytest httpx
Tests könnten für unser Beispiel dann etwa so aussehen:
# test.py from fastapi.testclient import TestClient from http import HTTPStatus from main import app client = TestClient(app) def test_read_root(): response = client.get("/") assert response.status_code == HTTPStatus.OK assert response.json() == {"Hello": "World"} def test_read_item(): response = client.get("/items/1") assert response.status_code == HTTPStatus.OK assert response.json() == {"item_id": 1, "q": None} def test_read_item_with_q(): response = client.get("/items/1?q=test") assert response.status_code == HTTPStatus.OK assert response.json() == {"item_id": 1, "q": "test"} def test_create_item(): json_payload = { "name": "Test Item", "description": "This is a test item", "price": 1, "tax": 2.5 } response = client.post("/items/", json=json_payload) assert response.status_code == HTTPStatus.OK assert response.json() == json_payload def test_read_cookie(): client.post("/cookie") # Set cookie response = client.get("/cookie") # Read cookie assert response.status_code == HTTPStatus.OK assert response.json() == {"user": "joel_geiser"}
Zur Ausführung der Tests reicht klassisch der pytest
-Befehl im Terminal:
(.venv)> pytest test.py
Antworten mit HTML
Unsere kleine API-Anwendung läuft, antwortet auf die Anfragen mit JSON und der Webbrowser zeigt die Daten direkt an. Das ist so zwar sehr effizient, doch im Webbrowser schon eher hässlich anzusehen und ein UX-Preis ist in weiter Ferne. Wir fügen einen neuen Endpunkt hinzu, der nun anstelle von JSON mit HTML antwortet. Aber keine Sorge, wir liefern jetzt nicht einfach statischen HTML-Code aus, sondern nutzen die Template-Engine Jinja2
die FastAPI freundlicherweise mit enthält. Jinja2
ermöglicht dynamische HTML-Seiten, wobei im HTML-Code Platzhalter definiert werden, die dann zur Laufzeit dynamisch ersetzt werden.
Hierfür erstelle ich einen neuen Ordner templates
, der die HTML-Templates beherbergt. Also erstellte ich folgende /templates/count.html
Datei:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>FastAPI-Counter</title> <style> html { display: table; margin: auto; text-align: center; } </style> </head> <body> <h1>{{ welcome }}</h1> <p> Wert: {{ counter }}</p> <button onclick="window.location.href=''">Klick mich</button> </body> </html>
Die Jinja2
–Platzhalter sind also direkt im HTML mit dem Syntax {{ variable
}}
definiert. Nun wird die Template-Engine in unsere Applikation hinzugefügt und im neuen Endpunkt «/count» eingesetzt. Zudem erstellen wir eine globale Variable counter
, damit wir einen dynamischen Wert haben und unser HTML damit abfüllen können.
from fastapi import FastAPI, Request from fastapi.templating import Jinja2Templates app = FastAPI() templates = Jinja2Templates(directory="templates") counter = 1 @app.get("/count") def count(request: Request): global counter counter += 1 context = {"request": request, "welcome": "Das ist mein Test", "counter": counter} return templates.TemplateResponse("count.html", context)
Im context
können wir nun beliebige Schlüsselwerte einfügen und der Template-Engine mitgeben. Wir setzten im context
also dynamisch für welcome
und value
Werte und Jinja2
ersetzt dann im HTML die entsprechenden Stellen. Damit können wir bequem unsere HTML, CSS, JS definieren und zur Laufzeit dynamisch abfüllen. Mein Beispielcode hier sollte im Browser etwa so aussehen:
Okay, das ist jetzt grafisch nicht viel schöner als rohes JSON – aber es geht an dieser Stelle eher ums Prinzip als die Ästhetik😉.
Statische Elemente
Falls statische Daten (CSS, JS, Bilder etc.) ein Thema sind, gibt es bei FastAPI ebenfalls eine sehr einfache und schnelle Lösung. Es reicht folgender Code, um eine Route zu definieren, die auf statische Daten innerhalb des Ordners static
zeigt:
app.mount("/static", StaticFiles(directory="static"), name="static")
Damit wird eine Route erstellt, womit direkt auf Dateien innerhalb des Verzeichnisses «/static» zugegriffen werden kann. So können wir nun unser CSS in eine style.css
auslagern. Im HTML-Template reicht wiederum etwas Jinja2-Code um die statische Ressource zu laden:
<link href="{{ url_for('static', path='/style.css') }}" rel="stylesheet">
Jinja
ersetzt die URL zur CSS-Datei und der Browser erhält folgendes Resultat:
<link href="http://localhost:8000/static/style.css" rel="stylesheet">
Damit sollten auch komplexeren HTML-Seiten nichts mehr im Wege stehen.
Fast und API
FastAPI hat sich in unserem Projekt als äusserst leistungsfähiges und effizientes Framework erwiesen. Durch die Kombination mit Pydantic
und SQLAlchemy
(ORM-Framework) konnten wir eine robuste und skalierbare Backend-Lösung entwickeln. Die intuitive Syntax und die automatische Dokumentation machen FastAPI auf jeden Fall zu einem sehenswerten Python-Web-Framework😎.
Ich hoffe, dieser Einblick inspiriert und hilft Ihnen bei Ihren eigenen Entwicklungen. Vielleicht gibt es hier auch noch eine Fortsetzung mit weiteren Insights…
Den Code gibt’s hier: https://github.com/JoelGeiser/fastapi-demo
Happy Coding!