""" 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