Wat is FastAPI?
FastAPI is een modern en goed presterend framework voor het bouwen van API’s in Python. Het is flexibel en gemakkelijk in gebruik. Qua performance is het gelijk aan Node.js en Go terwijl het het gebruiksgemak heeft van Python.
Het is gebouwd rondom Starlette (routering en async functionaliteit) en Pydantic (data validatie). Dit maakt het bouwen van robuuste oplossingen gemakkelijker.
FastAPI groeit daarom flink in populariteit en wordt bij veel bedrijven ingezet.
Klinkt goed, maar wat kan ik er mee?
Met behulp van dit framework kunnen koppelingen worden gebouwd tussen verschillende systemen. Het kan worden gebruikt te koppelen tussen systemen voor data uitwisseling en aansturing.
Voorbeelden van uiteindelijke oplossingen zijn:
- Een interface voor team overstijgende automatisering.
- Een interface om te voorzien in self service.
- Als portaal om data te leveren wat wordt gebruikt voor rapportage / compliancy.
Voorbeelden van implementaties
Ik heb voor diverse bedrijven FastAPI applicaties mogen ontwikkelen, voorbeelden zijn:
- Monitoring as Code orchestrator, single source of truth, die een monitoring platform cross tooling inricht en onderhoudt (desired state).
- Diverse workers voor het configureren en bewaking op configuration drift voor Grafana en Zabbix implementaties.
- Middleware tussen API’s en reporting tooling, zo blijft de reporting “snappy” en worden backend API’s niet teveel belast.
Automatische API documentatie
FastAPI voorziet in automatische documentatie van de API’s die je gebouwd hebt met behulp twee documentatie interfaces:
Swagger UI (/docs) – Hier vind je interactieve documentatie van de API. Je kunt de API hier ook testen om te zien wat de resultaten zijn voor een bepaalde query.
ReDoc (/redoc) – Vergelijkbaar met de Swagger UI alleen dan met een modernere UI met 3 panels.
Validatie van input
Door data te valideren wanneer het wordt ingegeven voorkom je problemen verderop in een process. Als je bevoorbeeld verwacht dat een integer wordt ingeven, dan kun je dit valideren.
- De waarde wordt gecontroleerd, is dit echt een integer.
- Zal een ingegeven numeriek string, als het een geheel getal is, omzetten naar een integer.
- Geeft een duidelijke error melding wanneer de validatie mislukt.
- Geeft de validatie argumenten weer in de API documentatie
Ondersteuning voor Asynchroniteit
Als de FastAPI is gebouwd op een ASGI server (Asychronous Server Gateway Interface) zoals uvicorn, dan geeft dit het ondersteuning voor async/await. Dit maakt het mogelijk om duizenden connecties tegelijkertijd te behandelen, in plaats van een voor een.
Performance
Bovenstaande en meer zorgt ervoor dat FastAPI erg goed presteert, en maakt het mogelijk om vrij gemakkelijk schaalbare applicaties te maken.
Bouw een eigen FastAPI applicatie
De minimale stappen voor het starten van een FastAPI applicatie zijn als volgt:
Installeer FastAPI en uvicorn:
pip install fastapi uvicorn[standard]
Een minimale FastAPI applicatie:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "query_param": q}
Uitleg bij de code
- Pad-parameters: De syntax
{item_id}in het URL-pad@app.get("/items/{item_id}")correspondeert direct met het functieargumentitem_id. - Typevalidatie: Door
item_id: intte definiëren, gebruikt FastAPI Python type hints om data automatisch te valideren. Als een gebruiker/items/foobezoekt, retourneert FastAPI een foutmelding omdat “foo” geen geheel getal (integer) is. - Query-parameters: Functieargumenten die niet expliciet in het pad zijn gedeclareerd (zoals
q), worden automatisch geïnterpreteerd als query-parameters (bijvoorbeeld/items/5?q=zoekopdracht). - Optionele waarden: Door de standaardwaarde op
Nonein te stellen, wordt de parameter optioneel.
Uitvoeren
Start de applicatie daarna met:
uvicorn main:app --reload
Open nu de volgende URL om de API documentatie te zien: http://localhost:8000/docs
Bouw een API voor productie
Een duidelijk datamodel is erg belangrijk voor het bouwen van een API voor productie. Dit zorgt ervoor dat de applicatie beheersbaar een efficient blijft. Dit voorkomt ook veel rework achteraf. Neem dus de tijd voor het ontwerp, dit verdien je later terug.
De modellen die je moet uitwerken zijn die voor verzoek (request) en antwoord (response).
from pydantic import BaseModel, EmailStr, Field, ConfigDict
from datetime import datetime
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
password: str = Field(..., min_length=8)
class UserResponse(BaseModel):
id: int
username: str
email: str
created_at: datetime
# De nieuwe syntax voor V2
model_config = ConfigDict(from_attributes=True)
@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
# Logica...
return new_user
Dit response model definieert responses met alleen specifieke velden, het geeft bijvoorbeeld niet het wachtwoord terug.
Authenticatie
Authenticatie, of inlogfunctionaliteit, is uiteraard erg belangrijk. Dit beschermd je applicatie tegen ongeoorloofde toegang.
In plaats van in elke functie te checken of een gebruiker is ingelogd, gebruik je Depends. FastAPI regelt dan dat de functie get_current_user eerst wordt uitgevoerd. Als die faalt (geen token), wordt de hoofd-functie nooit aangeroepen.
Dit model scheidt beveiliging van logica.
Dit is een voorbeeld van een authenticatieflow in FastAPI:
from typing import Annotated
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
app = FastAPI()
# 1. Define the Security Scheme (The 'Lock')
# This tells FastAPI that the client must send a Bearer token in the 'Authorization' header.
# The 'tokenUrl' parameter points to the endpoint where the client can obtain a token.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# 2. The User Model
class User(BaseModel):
username: str
email: str | None = None
disabled: bool | None = None
# 3. The Dependency (The 'Key Check')
# This function is automatically called by any endpoint that requires security.
# It receives the token from the request header and validates it.
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
# Simulate token decoding/validation (in production, use a JWT library like python-jose)
if token != "geheim-token-123":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
# If the token is valid, return the user object (this becomes the dependency value)
return User(username="johndoe", email="john@example.com", disabled=False)
# 4. Login Endpoint (Issues the token)
# This endpoint receives a standard OAuth2 form (username & password).
@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
# Here you would verify the username/password against your database (hashing, etc.)
if form_data.username == "johndoe" and form_data.password == "secret":
return {"access_token": "geheim-token-123", "token_type": "bearer"}
# If credentials are wrong, return a 400 error
raise HTTPException(status_code=400, detail="Incorrect username or password")
# 5. Protected Endpoint (Uses the dependency)
# This route requires the 'get_current_user' dependency to succeed.
@app.get("/users/me", response_model=User)
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]):
# This code runs ONLY if the token was valid.
# You have direct access to the validated 'current_user' object here.
return current_user
Foutafhandeling
In een professionele API wil je niet overal try/except blokken in je endpoints hebben. Het is schoner om Custom Exceptions te definiëren en deze op één plek (globaal) af te vangen.
Dit houdt je endpoints leesbaar: ze bevatten alleen de “Happy Path”.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
# 1. Define a Custom Exception
# This is a custom error class inheriting from Python's standard Exception.
# We can raise this specific error anywhere in our business logic.
class ThingException(Exception):
def __init__(self, name: str):
self.name = name
# 2. Register the Global Exception Handler
# This tells FastAPI: "Whenever 'ThingException' is raised anywhere in the application,
# catch it and execute this function instead of crashing."
@app.exception_handler(ThingException)
async def thing_exception_handler(request: Request, exc: ThingException):
return JSONResponse(
status_code=418, # I'm a teapot (using 418 as a custom example)
content={
"message": f"ERROR: The thing '{exc.name}' caused an issue.",
"error_code": "THING_ESCAPE_ERROR"
},
)
# 3. The Endpoint (Business Logic)
@app.get("/thing/{name}")
async def read_thing(name: str):
if name == "yolo":
# We do not need to construct a JSONResponse here manually.
# We simply raise the exception, and the global handler defined above intercepts it.
# This keeps the main logic clean and focused on the "Happy Path".
raise ThingException(name=name)
return {"thing_name": name}
Achtergrondtaken
Het is mogelijk om achtergrondtaken te laten uitvoeren door de FastAPI applicatie. Dit is nuttig voor langlopende taken of onderhoud binnen de applicatie, bijvoorbeeld voor het versturen van een rapportage of het verwijderen van records ouder dan x dagen.
Taak op gestart n.a.v. een request (fire and forget)
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
# 1. The Task (a standard Python function)
# This function defines the work to be done.
# It will execute *after* the HTTP response has been sent to the client.
def write_log_file(message: str):
with open("log.txt", "a") as f:
f.write(f"LOG: {message}\n")
# 2. The Endpoint
@app.post("/simple/")
async def run_task(background_tasks: BackgroundTasks):
# 3. Schedule the task
# We add the function 'write_log_file' to the queue with the specific argument.
# This is non-blocking; the code continues immediately.
background_tasks.add_task(write_log_file, "Button was pressed!")
# 4. Return an immediate response to the user
# The user gets a 200 OK instantly. The server starts processing the
# background task only after this response is delivered.
return {"message": "Task has been added to the queue"}
Geplande taak
from fastapi import FastAPI
from contextlib import asynccontextmanager
from apscheduler.schedulers.asyncio import AsyncIOScheduler
# 1. Initialize the scheduler
scheduler = AsyncIOScheduler()
# 2. Define the task (a function)
def daily_cleanup():
print("Time for cleanup.")
# 3. The Lifespan (Startup & Shutdown)
# This block manages what happens when the app starts and shuts down
@asynccontextmanager
async def lifespan(app: FastAPI):
# A. Schedule the job (interval, cron, or date)
# Here we schedule the task to run once every day
scheduler.add_job(daily_cleanup, 'interval', days=1)
# B. Start the scheduler
scheduler.start()
print("Scheduler started...")
yield # The application runs here...
# C. Stop the scheduler on shutdown
scheduler.shutdown()
print("Scheduler stopped.")
# 4. Attach lifespan to the app
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def root():
return {"message": "The app is running, check the console"}
SQLAlchemy en ORM
Hier is een voorbeeld van SQLAlchemy in een FastAPI-applicatie.
Dit voorbeeld een volledige implementatie van een database-operatie:
- Engine: De verbinding met de database.
- Model: De Python-klasse die de database-tabel voorstelt.
- Schema: De Pydantic-klasse voor validatie (zoals eerder besproken).
- Session: Het verzoek.
Ik gebruik hier sqlite zodat je het lokaal direct kunt draaien zonder installatie van een server, in productie zal je een database als MySQL of PostgreSQL gebruiken.
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, Session, declarative_base
from pydantic import BaseModel
# ==========================================
# 1. DATABASE CONFIGURATION
# ==========================================
# We use SQLite for this example (a file named 'sql_app.db')
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# The Engine is the entry point to the database
# connect_args is only needed for SQLite
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
# SessionLocal is a factory to create new database sessions
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base class for our ORM models
Base = declarative_base()
# ==========================================
# 2. SQLALCHEMY MODEL (The Database Table)
# ==========================================
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Integer, default=True)
# Create the tables in the database (usually done via migrations in prod)
Base.metadata.create_all(bind=engine)
# ==========================================
# 3. PYDANTIC SCHEMAS (Validation)
# ==========================================
class UserCreate(BaseModel):
email: str
password: str
class UserResponse(BaseModel):
id: int
email: str
is_active: bool
class Config:
orm_mode = True # Allows Pydantic to read data from the ORM object
# ==========================================
# 4. DEPENDENCY (Database Session Management)
# ==========================================
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app = FastAPI()
# ==========================================
# 5. CRUD OPERATIONS
# ==========================================
@app.post("/users/", response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
# Check if user already exists
# We query the User model directly
db_user = db.query(User).filter(User.email == user.email).first()
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
# Create the ORM Instance
fake_hashed_password = user.password + "notreallyhashed"
new_user = User(email=user.email, hashed_password=fake_hashed_password)
# Add to session, commit transaction, and refresh to get the ID
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
@app.get("/users/{user_id}", response_model=UserResponse)
def read_user(user_id: int, db: Session = Depends(get_db)):
# Fetch user by ID
db_user = db.query(User).filter(User.id == user_id).first()
if db_user is None:
raise HTTPException(status_code=404, detail="User not found")
return db_user
Hoe dit werkt (Conceptueel)
Het belangrijkste concept om te begrijpen is de Session.
- Request komt binnen: FastAPI ziet
db: Session = Depends(get_db). - De Dependency (
get_db) start: Er wordt een nieuwe database-sessie geopend (db = SessionLocal()). - De ORM doet zijn werk: In
create_uservertaal je Python objecten (new_user) naar SQL queries (INSERT INTO...). - Commit:
db.commit()maakt de wijzigingen permanent. - Refresh:
db.refresh()haalt de net gemaakte data (zoals de auto-generatedid) terug uit de database naar je Python object. - Cleanup: Zodra de request klaar is, zorgt de
finallyinget_dbervoor dat de verbinding wordt gesloten (db.close()).
Database connection tuning
De connectie met de database backend kan worden geoptimaliseerd door het toepassen van asychroniteit en pooling.
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import declarative_base
# Import the async connection string (e.g., postgresql+asyncpg://user:pass@localhost/db)
from .config import DATABASE_URL_ASYNC
# ============================================================
# ASYNCHRONOUS ENGINE
# ============================================================
# The engine is the primary entry point to the database.
# It manages the connection pool and dialect (SQL dialect).
async_engine = create_async_engine(
DATABASE_URL_ASYNC,
pool_size=10, # Keep 10 connections open by default (baseline)
max_overflow=20, # Allow up to 20 additional connections during peak load
pool_pre_ping=True, # Critical: Checks if the connection is alive before using it.
# Prevents "server has gone away" errors if the DB restarts.
pool_recycle=300 # Recycle connections every 300 seconds (5 minutes)
# to prevent firewalls from dropping idle connections.
)
# ============================================================
# ASYNCHRONOUS SESSION FACTORY
# ============================================================
# This factory generates a new Session for every incoming request.
AsyncSessionLocal = async_sessionmaker(
bind=async_engine,
class_=AsyncSession,
expire_on_commit=False # CRITICAL SETTING for Async:
# By default, SQLAlchemy expires objects after a commit.
# Accessing an expired attribute triggers a refresh (IO operation).
# Since attribute access (e.g., user.name) is synchronous,
# this would cause a crash in an async context.
# Setting this to False keeps data in memory after commit.
)
# ============================================================
# MODELS BASE
# ============================================================
# All database models (tables) must inherit from this class.
Base = declarative_base()
Om dit te gebruiken in API endpoints is het volgende nodig:
from typing import AsyncGenerator
# Dependency to get the database session
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
# Note: You should handle commits explicitly in your route logic
except Exception:
# If an error occurs, rollback the transaction
await session.rollback()
raise
finally:
# The context manager automatically closes the session,
# but explicit closing is good practice in some patterns.
await session.close()
Uitrollen van de FastAPI applicatie
Hoewel je de applicatie kunt draaien op een server of werkstation is dit vaak niet de geprefereerde manier. Vaak worden FastAPI applicaties opgenomen in een Docker image, om daarna met behulp van Docker, Podman of Kubernetes te deployen. Eventueel met tussenkomst van een CI/CD pipeline.
Docker file
FROM python:3.11-slim
WORKDIR /app
COPY . .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Conclusie
De kracht van FastAPI zit in de balans tussen performance en ontwikkelsnelheid.
FastAPI geeft het beste van twee werelden: de snelheid van Go of NodeJS, maar met de leesbaarheid van Python. Met minimale lappen code zet je in no-time een asynchrone API neer die klaar is voor productie.
Minder tijd kwijt aan debuggen en documenteren betekent meer tijd voor het bouwen van features die er echt toe doen.
In volgende posts wil ik de volgende onderwerpen behandelen:
- Observability van FastAPI applicaties
- FastAPI ingezet als frontend voor AI oplossingen.
- …