307 lines
8.5 KiB
Python
307 lines
8.5 KiB
Python
"""
|
|
code_first.py — FastAPI CRUD Beispiel (Code First OpenAPI)
|
|
|
|
Dieses Skript zeigt, wie man mit FastAPI eine kleine CRUD-API baut,
|
|
die automatisch eine OpenAPI-Dokumentation erzeugt (Swagger UI /docs).
|
|
|
|
Wichtige Idee (Code First):
|
|
- Wir schreiben zuerst den Code (Endpoints + Typen + Modelle).
|
|
- FastAPI generiert daraus automatisch die OpenAPI-Spezifikation.
|
|
- Swagger UI zeigt die Dokumentation interaktiv an.
|
|
|
|
Starten:
|
|
python3 -m uvicorn code_first:app --reload
|
|
|
|
Öffnen im Browser:
|
|
Swagger UI: http://127.0.0.1:8000/docs
|
|
OpenAPI JSON: http://127.0.0.1:8000/openapi.json
|
|
"""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 1) Imports: Welche Bausteine brauchen wir?
|
|
# -----------------------------------------------------------------------------
|
|
# FastAPI: das Framework
|
|
# HTTPException: um sauber Fehler (z.B. 404) zurückzugeben
|
|
# Path, Query: um Parameter in URL und Query sauber zu beschreiben/validieren
|
|
# status: komfortable HTTP-Statuscodes (z.B. 201, 204)
|
|
from fastapi import FastAPI, HTTPException, Path, Query, status
|
|
|
|
# Pydantic: Modelle für Datenvalidierung (Request/Response-Schemas)
|
|
# BaseModel: Basis für Datenmodelle
|
|
# Field: Metadaten zu Feldern (Beispiele, Constraints, Beschreibung)
|
|
from pydantic import BaseModel, Field
|
|
|
|
# typing: Typen für Listen etc. (OpenAPI profitiert davon)
|
|
from typing import List
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 2) App-Objekt: Das ist "die API"
|
|
# -----------------------------------------------------------------------------
|
|
# `app` ist der Einstiegspunkt:
|
|
# - Alle Endpoints (@app.get, @app.post, ...) hängen an diesem Objekt.
|
|
# - FastAPI sammelt daraus automatisch die OpenAPI-Doku.
|
|
app = FastAPI(
|
|
title="User Service API",
|
|
description="Simple CRUD API to demonstrate Code First OpenAPI generation with FastAPI",
|
|
version="1.0.0",
|
|
)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 3) Datenmodelle (Pydantic): Der "Vertrag" der API
|
|
# -----------------------------------------------------------------------------
|
|
# FastAPI nutzt diese Modelle für:
|
|
# - Validierung von eingehenden Daten (Request Body)
|
|
# - automatische Erzeugung der OpenAPI-Schemas
|
|
# - Ausgabeformat (Response) über response_model
|
|
#
|
|
# WICHTIG:
|
|
# "Pydantic macht unsere Typannotationen ausführbar: es prüft Daten zur Laufzeit."
|
|
|
|
|
|
class UserBase(BaseModel):
|
|
"""
|
|
Basis-Modell für einen User.
|
|
|
|
Enthält die Felder, die sowohl beim Erstellen als auch beim Zurückgeben
|
|
eines Users (Response) vorkommen.
|
|
|
|
Warum "Base"?
|
|
- Wir können Modelle wiederverwenden und Duplikate vermeiden.
|
|
"""
|
|
|
|
# Field(...): bedeutet "Pflichtfeld" (required)
|
|
# json_schema_extra: Beispielwerte für Swagger/OpenAPI (Pydantic v2-konform)
|
|
name: str = Field(
|
|
...,
|
|
description="Der Name des Users",
|
|
json_schema_extra={"example": "Alice"},
|
|
)
|
|
age: int = Field(
|
|
...,
|
|
description="Das Alter des Users (als Zahl)",
|
|
json_schema_extra={"example": 22},
|
|
ge=0, # ge = greater or equal: verhindert negative Alterswerte
|
|
)
|
|
email: str = Field(
|
|
...,
|
|
description="E-Mail-Adresse des Users",
|
|
json_schema_extra={"example": "alice@example.com"},
|
|
)
|
|
|
|
|
|
class UserCreate(UserBase):
|
|
"""
|
|
Modell für das Erstellen eines Users.
|
|
|
|
In diesem Beispiel ist es identisch zu UserBase.
|
|
Warum trotzdem eine eigene Klasse?
|
|
- In echten Projekten unterscheiden sich Create-Modelle oft (z.B. kein 'id').
|
|
- So bleibt die API sauber erweiterbar, ohne später alles umzubauen.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class User(UserBase):
|
|
"""
|
|
Modell für den User, so wie er von der API zurückgegeben wird.
|
|
|
|
Unterschied zu UserCreate:
|
|
- 'id' kommt hinzu (die wird serverseitig vergeben).
|
|
"""
|
|
|
|
id: int = Field(
|
|
...,
|
|
description="Eindeutige ID des Users",
|
|
json_schema_extra={"example": 1},
|
|
ge=1,
|
|
)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 4) Fake-Datenbank (In-Memory)
|
|
# -----------------------------------------------------------------------------
|
|
# Für den Workshop nutzen wir absichtlich keine echte Datenbank,
|
|
# damit jeder die API sofort lokal starten kann.
|
|
#
|
|
# Wichtig:
|
|
# - Daten sind weg, sobald der Server neu startet.
|
|
# - Nicht threadsafe/produktionsreif.
|
|
# - Für Demo/Workshop absolut perfekt.
|
|
users_db: dict[int, User] = {}
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 5) CRUD Endpoints
|
|
# -----------------------------------------------------------------------------
|
|
# CRUD = Create, Read, Update, Delete
|
|
#
|
|
# Wir benutzen HTTP-Methoden:
|
|
# - POST => Create
|
|
# - GET => Read (list / detail)
|
|
# - PUT => Update
|
|
# - DELETE => Delete
|
|
#
|
|
# FastAPI generiert aus den folgenden Decorators automatisch OpenAPI:
|
|
# - Pfade (/users, /users/{user_id})
|
|
# - Methoden (GET/POST/PUT/DELETE)
|
|
# - Parameter (Path/Query)
|
|
# - Request Body (Pydantic-Model)
|
|
# - Response-Model (response_model=...)
|
|
# - Statuscodes, Summary, Description, Tags
|
|
|
|
|
|
@app.post(
|
|
"/users",
|
|
response_model=User,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create a new user",
|
|
description="Creates a new user and returns the created resource",
|
|
tags=["Users"],
|
|
)
|
|
def create_user(user: UserCreate):
|
|
"""
|
|
CREATE: Einen neuen User anlegen.
|
|
|
|
Request:
|
|
- Body enthält ein UserCreate JSON (name, age, email)
|
|
- FastAPI validiert automatisch (z.B. age muss int sein)
|
|
|
|
Response:
|
|
- 201 Created
|
|
- User (inkl. id)
|
|
|
|
Hinweis:
|
|
- Wir generieren die ID hier simpel über `len(users_db) + 1`.
|
|
Das ist für Demo ok, aber in Produktion würde das die Datenbank übernehmen.
|
|
"""
|
|
user_id = len(users_db) + 1
|
|
|
|
# Pydantic v2: model_dump() statt dict()
|
|
new_user = User(id=user_id, **user.model_dump())
|
|
|
|
users_db[user_id] = new_user
|
|
return new_user
|
|
|
|
|
|
@app.get(
|
|
"/users",
|
|
response_model=List[User],
|
|
summary="List all users",
|
|
description="Returns a list of all users",
|
|
tags=["Users"],
|
|
)
|
|
def list_users(
|
|
limit: int = Query(
|
|
10,
|
|
ge=1,
|
|
le=100,
|
|
description="Maximum number of users to return",
|
|
)
|
|
):
|
|
"""
|
|
READ (List): Alle Users zurückgeben.
|
|
|
|
Query-Parameter:
|
|
- limit: wie viele Users maximal zurückgegeben werden
|
|
- Default: 10
|
|
- Minimum: 1
|
|
- Maximum: 100
|
|
|
|
Response:
|
|
- 200 OK
|
|
- Liste von Users
|
|
"""
|
|
return list(users_db.values())[:limit]
|
|
|
|
|
|
@app.get(
|
|
"/users/{user_id}",
|
|
response_model=User,
|
|
summary="Get user by ID",
|
|
description="Returns a single user by its ID",
|
|
tags=["Users"],
|
|
)
|
|
def get_user(
|
|
user_id: int = Path(
|
|
...,
|
|
ge=1,
|
|
description="The ID of the user",
|
|
)
|
|
):
|
|
"""
|
|
READ (Detail): Einen User anhand seiner ID holen.
|
|
|
|
Path-Parameter:
|
|
- user_id: ID in der URL, z.B. /users/3
|
|
|
|
Wenn der User nicht existiert:
|
|
- 404 Not Found mit {"detail": "User not found"}
|
|
"""
|
|
user = users_db.get(user_id)
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return user
|
|
|
|
|
|
@app.put(
|
|
"/users/{user_id}",
|
|
response_model=User,
|
|
summary="Update a user",
|
|
description="Updates an existing user",
|
|
tags=["Users"],
|
|
)
|
|
def update_user(
|
|
user_id: int = Path(..., ge=1, description="The ID of the user"),
|
|
user: UserCreate = ...,
|
|
):
|
|
"""
|
|
UPDATE: Einen bestehenden User vollständig überschreiben.
|
|
|
|
PUT bedeutet in der Regel:
|
|
- Der Client schickt den kompletten Datensatz (name, age, email)
|
|
- Der Server ersetzt den existierenden Eintrag vollständig
|
|
|
|
Wenn der User nicht existiert:
|
|
- 404 Not Found
|
|
|
|
Response:
|
|
- 200 OK
|
|
- Aktualisierter User (inkl. id)
|
|
"""
|
|
if user_id not in users_db:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
updated_user = User(id=user_id, **user.model_dump())
|
|
users_db[user_id] = updated_user
|
|
return updated_user
|
|
|
|
|
|
@app.delete(
|
|
"/users/{user_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Delete a user",
|
|
description="Deletes a user by its ID",
|
|
tags=["Users"],
|
|
)
|
|
def delete_user(
|
|
user_id: int = Path(..., ge=1, description="The ID of the user"),
|
|
):
|
|
"""
|
|
DELETE: Einen User löschen.
|
|
|
|
Wenn der User nicht existiert:
|
|
- 404 Not Found
|
|
|
|
Response:
|
|
- 204 No Content (kein Response-Body)
|
|
-> Das ist üblich bei erfolgreichen DELETEs.
|
|
"""
|
|
if user_id not in users_db:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
del users_db[user_id]
|
|
# Bei 204 geben wir keinen Inhalt zurück.
|
|
return |