# Web-APIs mit Flask: von der App zur JSON-Schnittstelle

URL: https://www.mikebild.dev/de/blog/web-apis-mit-flask/

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.

```python
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:

```python
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, und `methods` legt fest, welche HTTP-Verben die Route bedient.
- Request: Das importierte Objekt `request` beschreibt die eingehende Anfrage – `request.args` liest den Query-String, `request.form` die Formularfelder, `request.json` den JSON-Rumpf, `request.method` das 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:

```python
@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.

```mermaid
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.

```python
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:

```python
@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.

```python
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:

```html
<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.

```mermaid
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.

```python
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`.

```python
@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`, Verzeichnis `web/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()`](https://flask.palletsprojects.com/en/stable/api/#flask.Request.get_json)
