python
from contextlib import asynccontextmanager
from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from db import pool
@asynccontextmanager
async def lifespan(app: FastAPI):
pool.open() # beim Start der App
yield
pool.close() # beim Beenden
app = FastAPI(lifespan=lifespan)
templates = Jinja2Templates(directory='templates')
# ---------- HTML-Seiten ----------
@app.get('/', response_class=HTMLResponse)
def kundenliste(request: Request, q: str = ''):
with pool.connection() as conn:
kunden = conn.execute(
'SELECT id, name, stadt, umsatz FROM kunden WHERE name ILIKE %s ORDER BY name',
(f'%{q}%',),
).fetchall()
return templates.TemplateResponse(request, 'kunden.html', {'kunden': kunden, 'q': q})
@app.post('/kunden')
def kunde_anlegen(name: str = Form(...), stadt: str = Form('')):
with pool.connection() as conn:
conn.execute(
'INSERT INTO kunden (name, stadt) VALUES (%s, %s)',
(name, stadt or None),
)
return RedirectResponse('/', status_code=303) # zurück zur Liste
# ---------- JSON-API ----------
class KundeNeu(BaseModel):
name: str
stadt: str | None = None
@app.get('/api/kunden')
def api_liste():
with pool.connection() as conn:
return conn.execute('SELECT id, name, stadt, umsatz FROM kunden ORDER BY id').fetchall()
@app.get('/api/kunden/{kunden_id}')
def api_einzeln(kunden_id: int):
with pool.connection() as conn:
kunde = conn.execute(
'SELECT id, name, stadt, umsatz FROM kunden WHERE id = %s', (kunden_id,)
).fetchone()
if kunde is None:
raise HTTPException(status_code=404, detail='Kunde nicht gefunden')
return kunde
@app.post('/api/kunden', status_code=201)
def api_anlegen(kunde: KundeNeu):
with pool.connection() as conn:
return conn.execute(
'INSERT INTO kunden (name, stadt) VALUES (%s, %s) RETURNING id, name, stadt',
(kunde.name, kunde.stadt),
).fetchone()
Starten mit DATABASE_URL=postgresql://lernen:geheim@localhost:5432/lerndb uv run fastapi dev main.py. Die Tabelle kunden hast du in Kapitel 12 angelegt.
Was passiert:
lifespanöffnet beim Start den Verbindungspool und schließt ihn beim Beenden. Alles voryieldläuft beim Start, alles danach beim Ende.- Die Routen sind normale Funktionen (
def, nichtasync def). FastAPI führt sie in einem Thread-Pool aus, daher blockiert der Datenbankzugriff mitpsycopgnichts anderes. So bleibst du ohneasynceinfach. templates.TemplateResponse(request, 'kunden.html', {...})füllt das HTML-Template mit dem Dictionary und gibt die Seite zurück.- Nach einem POST leitest du mit Status
303zurück auf eine GET-Seite weiter (Post-Redirect-Get). Sonst würde ein Neuladen der Seite das Formular erneut abschicken. KundeNeu(BaseModel)ist ein Pydantic-Modell. FastAPI liest den JSON-Body, prüft Typen und Pflichtfelder und liefert bei Fehlern automatisch422.raise HTTPException(status_code=404, ...)bricht ab und antwortet mit dem Statuscode.