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.
In fast jeder meiner Python-Schulungen kommt irgendwann der Moment, in dem jemand fragt: „Und wie mache ich daraus jetzt eine Web-Schnittstelle?" Meist steht dann schon ein Skript da, das Daten einliest, etwas berechnet und ein Ergebnis produziert. Was fehlt, ist der Weg von diesem Skript zu etwas, das ein anderes Programm über HTTP ansprechen kann. Genau an dieser Stelle greife ich zu Flask – nicht weil es das mächtigste Framework wäre, sondern weil sich in wenigen Zeilen nachvollziehen lässt, was zwischen einem HTTP-Request und meiner Funktion tatsächlich passiert.
Flask ist ein Mikro-Framework. Es bringt Routing, ein Request-Objekt und eine Template-Engine mit, und lässt fast alles andere offen. Für die Ausbildung ist das ideal: Es gibt kaum Magie, die man erst wieder auseinandernehmen müsste, um zu verstehen, was passiert. Ich beziehe mich in diesem Text 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 hier schnell zurecht. Falls die Grundlagen noch wackeln, lohnt vorab ein Blick auf Python für Entwickler, die Systeme verstehen wollen.
Hello World, und was dahinter steckt
Der kleinste sinnvolle Flask-Server passt in sechs Zeilen. Ich zeige ihn immer zuerst, weil sich daran das gesamte Modell erklären lässt.
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 ich zur eigentlichen API komme, sortiere ich in der Schulung gern drei Begriffe, 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. Ich zeichne es oft an, weil es zeigt, dass zwischen der ankommenden Anfrage und der fertigen Antwort nur wenige, klar benannte Schritte stehen.
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 sich RESTful denken zeigt: 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. Ich erwähne das, weil man in vielen Tutorials und Stack-Overflow-Antworten beide Varianten findet und leicht Version und Verhalten durcheinanderbringt. Meine Empfehlung in der Schulung: 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
Ein Missverständnis, das ich oft ausräume: dass serverseitiges Rendern (SSR) und eine JSON-API zwei verschiedene Welten seien. In Flask sind sie es nicht. Es ist 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. Diese Verzweigung zeichne ich als zweites Diagramm, weil sie den Kern gut einfängt.
graph LR
A[View Handler] --> B{Repräsentation?}
B -->|render_template| C[HTML]
B -->|jsonify| D[JSON]
Wer das einmal verinnerlicht hat, hört auf, „API-Framework" und „Web-Framework" als getrennte Kategorien zu behandeln. 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, die ich immer anspreche
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. Schickt der Client keinen Header Content-Type: application/json, ist request.json schlicht None, und ein direkter Zugriff wie payload['name'] läuft in einen Fehler. Deshalb prüfe ich den Rumpf, bevor ich ihn benutze, oder nutze request.get_json(silent=True), das im Zweifel None liefert statt eine Ausnahme zu werfen.
@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 im Kurs 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.
Was ich Teilnehmenden mitgebe, ist weniger eine Liste von Funktionen als eine Haltung: Man sollte jederzeit sagen können, was zwischen dem HTTP-Request und der eigenen Funktion geschieht. Flask macht diese Kette sichtbar. Wer sie verstanden hat, kann später auch komplexere Frameworks lesen, ohne sich von deren Komfort blenden zu lassen – denn darunter steht überall dasselbe Modell aus Route, Request und Response.
Weiterführende Quellen
- 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/
Kommentare
Kommentar schreiben