add code first file

main
Fabian Hoppe 2026-02-10 13:44:26 +01:00
parent 5f98b22da3
commit 4c8dc27ba9
1 changed files with 307 additions and 0 deletions

View File

@ -0,0 +1,307 @@
"""
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