en / de
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

FastAPI in action: Building a modern web API

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 Python v3.8 oder neuer auf dem System verfügbar, erstellen wir zunächst eine Python-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 HTTPFehler geworfen, ansonsten wird der Wert dargestellt. 

POST-Anfrage, wo das Cookie gesetzt wird.

GET-Anfrage, wo das Cookie ausgelesen wird.

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 Jinja2Platzhalter 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!

Kommentare

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden
NACH OBEN
Zur Webcast Übersicht