Web-APIs mit Flask: von der App zur JSON-Schnittstelle
Von Hello World zur JSON-API: wie Routing, Request-Verarbeitung und jsonify in Flask 1.1 zusammenspielen – und warum SSR und API dieselbe Mechanik sind.
Drei Begriffe genügen, um alles zu beschreiben, was Flask tut: Route, Request, Response. Ein Dekorator bindet einen URL-Pfad an eine Python-Funktion, das Objekt request beschreibt die eingehende Anfrage, und der Rückgabewert der Funktion wird zur Antwort. Templates, Blueprints, JSON – alles Weitere hängt an diesen drei Punkten.
Damit ist Flask ein guter Ausgangspunkt, wenn aus einem Python-Skript eine Schnittstelle werden soll, die ein anderes Programm über HTTP ansprechen kann. Es ist ein Mikro-Framework: Routing, Request-Objekt und Template-Engine sind dabei, fast alles andere bleibt offen. Es gibt kaum Magie, die man erst wieder auseinandernehmen müsste, um zu verstehen, was zwischen HTTP-Request und eigener Funktion tatsächlich passiert. Dieser Text bezieht sich durchgehend auf Flask 1.1 – die stabile Serie zum Zeitpunkt des Schreibens (1.1.2 erschien im April 2020). Wer schon einmal mit Python-Objekten, Dekoratoren und Dictionaries gearbeitet hat, findet sich schnell zurecht.
Redaktionelle Einordnung (Juli 2026): Beim heutigen Flask 3.1 ruft request.json intern get_json() auf und antwortet bei einem ungeeigneten Content-Type standardmäßig mit 415. Nur request.get_json(silent=True) liefert in diesem Fall None. Diese Änderung betrifft die Fehlerbehandlung, nicht das Routing-Modell des Artikels.
Hello World, und was dahinter steckt
Der kleinste sinnvolle Flask-Server passt in sechs Zeilen. An ihm lässt sich das gesamte Modell ablesen.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run(debug=True, port=8080)
Flask(__name__) erzeugt die Anwendung. Der Name des Moduls dient Flask später als Ausgangspunkt, um etwa Templates und statische Dateien relativ zum Projekt zu finden. Der Dekorator @app.route('/') verknüpft einen URL-Pfad mit einer Funktion. Wird der Server unter / aufgerufen, ruft Flask hello_world() auf und schickt den Rückgabewert als Antwort zurück. app.run() startet den eingebauten Entwicklungsserver.
Der zentrale Gedanke: Eine Funktion, die einen String zurückgibt, wird zu einer HTTP-Antwort mit Content-Type: text/html. Flask nimmt mir die gesamte Übersetzung zwischen HTTP und Python-Funktion ab. Und diese Übersetzung ist flexibler, als der erste Eindruck vermuten lässt. Ein View darf nicht nur einen String zurückgeben, sondern auch ein Tuple aus Rumpf und Statuscode:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!', 202
Hier bekommt der Client denselben Text, aber mit dem Status 202 statt der Standard-200. Das Tuple kann sogar drei Elemente haben – Rumpf, Status und ein Dictionary mit zusätzlichen Headern. Diese Rückgabe-Konventionen sind der Kern von Flask, und sie erklären, warum sich dieselbe Route mal als HTML-Seite und mal als JSON-Schnittstelle verhält.
Routing, Request, Response
Bevor die eigentliche API dran ist, lohnen die drei Begriffe eine genauere Betrachtung, weil sie durchgängig wiederkehren:
- Route: Der Dekorator
@app.route(pfad, methods=[...])bindet einen URL-Pfad an eine Funktion. Pfad-Parameter wie<int:item_id>reichen Teile der URL direkt an die Funktion weiter, undmethodslegt fest, welche HTTP-Verben die Route bedient. - Request: Das importierte Objekt
requestbeschreibt die eingehende Anfrage –request.argsliest den Query-String,request.formdie Formularfelder,request.jsonden JSON-Rumpf,request.methoddas verwendete Verb. - Response: Der Rückgabewert der Funktion. Ein String wird zu HTML,
jsonify(...)zu JSON, ein Tuple(rumpf, status)steuert den Statuscode.
Diese drei Achsen genügen, um erstaunlich viel zu bauen. Ein Pfad mit Parameter sieht etwa so aus:
@app.route('/user/<username>')
def profile(username):
return f'Profile page of {username}'
Der Platzhalter <username> wird als Argument an die Funktion übergeben. Braucht man einen numerischen Wert, verlangt der Konverter <int:item_id> eine Ganzzahl und liefert sie bereits als int – passt der Pfad nicht auf dieses Muster, antwortet Flask von selbst mit 404, bevor meine Funktion überhaupt läuft.
Der Request-Lebenszyklus lässt sich in einem einfachen Diagramm zusammenfassen. Zwischen der ankommenden Anfrage und der fertigen Antwort stehen nur wenige, klar benannte Schritte.
graph LR
A[HTTP Request] --> B["@app.route<br/>Matching"]
B --> C[View Handler]
C --> D["jsonify()"]
D --> E[JSON Response]
Der Request trifft ein, Flask sucht die passende Route, ruft die zugehörige Funktion auf, diese baut mit jsonify eine Antwort, und die geht an den Client zurück. Kein Schritt davon ist versteckt – man kann an jeder Stelle einen print setzen und zusehen.
Von der Seite zur Schnittstelle
Jetzt zum eigentlichen Ziel: eine kleine, REST-artige Schnittstelle. Ich baue dafür einen einzigen Pfad, der zwei Verben bedient – ein GET liefert die Liste aller Einträge, ein POST legt einen neuen an. Dass eine URL mehrere Methoden bedienen kann, ist genau der Punkt, an dem REST-Denken sichtbar wird: Die Ressource ist dieselbe, nur die Aktion darauf unterscheidet sich.
from flask import Flask, request, jsonify
app = Flask(__name__)
items = [] # demo-only in-memory store
@app.route('/items', methods=['GET', 'POST'])
def items_collection():
if request.method == 'POST':
payload = request.get_json()
item = {'id': len(items) + 1, 'name': payload['name']}
items.append(item)
return jsonify(item), 201
return jsonify(items) # GET
@app.route('/items/<int:item_id>')
def item_detail(item_id):
match = next((i for i in items if i['id'] == item_id), None)
return (jsonify(match), 200) if match else ('Not found', 404)
Ein paar Dinge sind hier bewusst gesetzt. Beim POST lese ich den JSON-Rumpf mit request.get_json() und antworte mit Status 201, dem Code für „neu erstellt“. Beim GET gebe ich die gesamte Liste zurück. Und die zweite Route zeigt den Pfad-Parameter im Einsatz: /items/3 sucht den Eintrag mit dieser ID und liefert entweder ihn oder eine 404-Antwort.
jsonify ist dabei mehr als ein hübscheres json.dumps. Es serialisiert das Dictionary oder die Liste, setzt den Content-Type korrekt auf application/json und baut ein fertiges Response-Objekt. Der handgemachte Weg – json.dumps(...) in einen String verwandeln und den Header manuell setzen – funktioniert zwar, ist aber fehleranfälliger und dupliziert genau die Arbeit, die Flask ohnehin sauber erledigt.
Der historische Dreh: dict direkt zurückgeben
Hier lohnt eine Notiz, die in älteren Beispielen für Verwirrung sorgt. Seit Flask 1.1 darf ein View ein dict direkt zurückgeben – Flask ruft dann intern automatisch jsonify() darauf auf:
@app.route('/status')
def status():
return {'ok': True, 'items': len(items)} # Flask >= 1.1 -> JSON
Das ist bequem, aber es ist eine relativ junge Erleichterung. In Flask 1.0 und davor war genau diese Zeile ein Fehler: Ein Dictionary war kein gültiger Rückgabetyp, und man bekam einen TypeError. Wer damals JSON ausliefern wollte, musste return jsonify({...}) explizit schreiben. In Tutorials und Stack-Overflow-Antworten stehen beide Varianten nebeneinander, ohne dass die Version dabeisteht – so geraten Version und Verhalten leicht durcheinander. Solange nicht sicher ist, welche Flask-Version läuft, schreibe ich jsonify explizit hin. Es funktioniert in jeder Version und macht die Absicht deutlich.
SSR und API sind dieselbe Mechanik
Serverseitiges Rendern (SSR) und eine JSON-API gelten oft als zwei verschiedene Welten. In Flask sind sie es nicht: derselbe Routing-Mechanismus, dieselbe Funktion, nur ein anderer Rückgabetyp. Statt jsonify verwende ich render_template, das eine HTML-Datei aus dem Ordner templates/ mit Jinja2 füllt.
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', items=items)
@app.route('/forms', methods=['POST'])
def forms():
return 'You said: ' + request.form['text']
Das zugehörige Template ist gewöhnliches HTML mit Platzhaltern. Der übergebene Kontext – hier items – steht in der Vorlage als Variable zur Verfügung:
<form action="/forms" method="POST">
<input name="text">
<input type="submit" value="Submit">
</form>
<ul>
{% for item in items %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
Der Unterschied zwischen der API und der gerenderten Seite ist allein die Repräsentation: einmal JSON für Maschinen, einmal HTML für den Browser. Das Framework darunter bleibt gleich. Als Diagramm ist die Verzweigung schnell erzählt.
graph LR
A[View Handler] --> B{Repräsentation?}
B -->|render_template| C[HTML]
B -->|jsonify| D[JSON]
Damit lösen sich „API-Framework“ und „Web-Framework“ als getrennte Kategorien auf. Es ist eine Entscheidung pro Route, kein Architekturbruch.
Struktur mit Blueprints
Sobald mehr als eine Handvoll Routen zusammenkommt, wird die eine große app.py unübersichtlich. Flask bietet dafür Blueprints – eine Möglichkeit, Routen thematisch zu gruppieren und mit einem gemeinsamen Präfix an die App zu hängen.
from flask import Blueprint, jsonify
api = Blueprint('api', __name__, url_prefix='/api')
@api.route('/items')
def list_items():
return jsonify(items)
# in the main app:
# app.register_blueprint(api)
Alle Routen dieses Blueprints liegen dann unter /api – die Route oben ist also unter /api/items erreichbar. Wichtig zu verstehen: Ein Blueprint ist ein reines Strukturmittel. Er ändert nichts am Laufzeitverhalten und bringt keine eigene Magie mit, sondern hilft nur, den Code in nachvollziehbare Einheiten zu schneiden. Ich trenne damit gern die JSON-API von den gerenderten Seiten – zwei Blueprints, ein Projekt.
Drei Fallstricke
Zum Schluss die Stellen, an denen es in der Praxis regelmäßig klemmt. Keiner dieser Punkte ist ein Bug in Flask – es sind Eigenheiten, die man kennen muss.
Der erste betrifft request.json. Im hier verwendeten Flask 1.1 war der Wert ohne Content-Type: application/json schlicht None, und ein direkter Zugriff wie payload['name'] lief in einen Fehler. Aktuelle Flask-Versionen antworten bei diesem Zugriff standardmäßig mit 415. Wer bewusst selbst behandeln will, nutzt request.get_json(silent=True) und prüft das mögliche None.
@app.route('/echo', methods=['POST'])
def echo():
data = request.get_json(silent=True)
if not data:
return jsonify({'error': 'expected JSON body'}), 400
return jsonify(data)
Der zweite ist debug=True. Im Entwicklungsserver ist der Debug-Modus praktisch – er lädt bei Änderungen neu und zeigt bei Fehlern einen interaktiven Traceback. Genau dieser interaktive Traceback ist aber eine ausführbare Konsole im Browser. In einer öffentlich erreichbaren Umgebung ist das eine offene Tür für das Ausführen beliebigen Codes. debug=True gehört ausschließlich in die lokale Entwicklung. Und app.run() selbst ist ohnehin nur der Entwicklungsserver – für den produktiven Betrieb setzt man einen WSGI-Server wie gunicorn oder uWSGI davor.
Der dritte betrifft den globalen Zustand. Meine items = []-Liste ist eine Demo-Abkürzung und keine Datenbank. Sie steht im Speicher des Prozesses, geht bei jedem Neustart verloren und verhält sich unzuverlässig, sobald mehrere Worker parallel laufen – dann sieht jeder Worker seine eigene Liste. Für ein Beispiel wie dieses ist das völlig in Ordnung, für echte Daten braucht es Persistenz, etwa eine Datenbank hinter der Route.
Fazit
Der Weg von einem Python-Skript zu einer Web-Schnittstelle ist mit Flask kürzer, als viele erwarten, und – wichtiger – er ist nachvollziehbar. Eine Funktion wird über einen Dekorator an einen Pfad gebunden, das request-Objekt beschreibt die Anfrage, der Rückgabewert wird zur Antwort. Ob daraus eine HTML-Seite über render_template oder eine JSON-Schnittstelle über jsonify wird, ist eine Entscheidung pro Route und kein Systemwechsel. Blueprints geben dem Ganzen Struktur, sobald es wächst, ohne neue Konzepte einzuführen.
Wichtiger als die Liste der Funktionen ist mir dabei, dass sich jederzeit sagen lässt, was zwischen dem HTTP-Request und der eigenen Funktion geschieht. Flask hält diese Kette sichtbar. Wer sie einmal durchgegangen ist, liest auch komfortablere Frameworks gelassener – darunter steht überall dasselbe Modell aus Route, Request und Response.
Weiterführende Quellen
- Foliensatz zu diesem Thema: https://supabase.mikebild.dev/storage/v1/object/public/slides/introduction-python.html
- Repository
introduction-python, Verzeichnisweb/examples/– die lauffähigen Flask-Beispiele (1-app.py,2-html.py,3-json.py,app.py), auf denen dieser Artikel aufbaut. - Flask 1.1 Quickstart – Routing, Methoden und Pfad-Parameter: https://flask.palletsprojects.com/en/1.1.x/quickstart/
- Flask 1.1 API-Referenz – Request-Objekt und
jsonify: https://flask.palletsprojects.com/en/1.1.x/api/ - Flask 1.1 Blueprints: https://flask.palletsprojects.com/en/1.1.x/blueprints/
- Flask 1.1 Deployment – warum der Entwicklungsserver nicht in Produktion gehört: https://flask.palletsprojects.com/en/1.1.x/deploying/
- Flask 3.1: eingehende Request-Daten und
get_json()
Wie fandest du diesen Beitrag?
Kommentare