REST API’s bouwen met Python: van ontwerp tot implementatie
Een REST API bouwen is meer dan “een endpoint maken dat JSON teruggeeft”. Een goede API is voorspelbaar, veilig, goed te testen, makkelijk te onderhouden en duidelijk voor gebruikers. In deze tutorial doorlopen we het volledige traject: van ontwerpkeuzes en datamodellering tot implementatie met Python, validatie, authenticatie, versiebeheer, documentatie, testen, logging en deployment.
We gebruiken FastAPI omdat het modern is, snelle feedback geeft via automatische documentatie, en sterk is in type hints en validatie. De concepten (resources, HTTP-methodes, statuscodes, idempotentie, paginatie) zijn echter algemeen en gelden ook voor Flask, Django REST Framework of andere frameworks.
Inhoud
- Wat is REST (en wat niet)
- API-ontwerp: resources, routes en conventies
- HTTP-methodes, statuscodes en idempotentie
- Datamodellen en validatie met Pydantic
- Projectstructuur en afhankelijkheden
- Implementatie met FastAPI
- Database-laag met SQLAlchemy en migraties
- CRUD-endpoints bouwen
- Foutenafhandeling en consistente responses
- Authenticatie en autorisatie (JWT)
- Paginatie, filtering en sortering
- Versiebeheer van je API
- OpenAPI-documentatie en voorbeelden
- Testen met pytest
- Logging, configuratie en omgevingsvariabelen
- Deployment met Docker
- Checklist voor productie
Wat is REST (en wat niet)
REST is een architectuurstijl voor netwerkapplicaties. In de praktijk betekent dit meestal:
- Je modelleert je domein als resources (bijvoorbeeld
users,orders,products). - Je gebruikt HTTP als applicatieprotocol, inclusief methodes (GET/POST/PUT/PATCH/DELETE), statuscodes, headers en caching.
- Je gebruikt representaties (meestal JSON) om resources over te dragen.
- Je API is stateless: elke request bevat voldoende informatie om verwerkt te worden, zonder server-side sessiestatus.
Wat REST niet automatisch betekent:
- “Alles is een endpoint dat iets doet” (RPC-stijl). REST is resource-georiënteerd, niet actie-georiënteerd.
- “Altijd 200 OK teruggeven”. Statuscodes zijn betekenisvol en belangrijk.
- “JSON is genoeg”. Ook consistentie, validatie, beveiliging en documentatie zijn essentieel.
API-ontwerp: resources, routes en conventies
Resources kiezen
Begin met het identificeren van zelfstandige entiteiten in je domein. Stel: we bouwen een eenvoudige taken-app.
Mogelijke resources:
userstasks
Een task heeft bijvoorbeeld:
idtitledescriptiondonecreated_atowner_id
URL-conventies
Gebruik zelfstandige naamwoorden (meervoud) en hiërarchie waar logisch:
/tasksvoor de collectie/tasks/{task_id}voor een specifiek item/users/{user_id}/tasksals je taken per gebruiker wil benaderen
Vermijd werkwoorden in paden zoals /createTask of /doLogin. Acties worden uitgedrukt via HTTP-methodes of via subresources als het echt nodig is.
Representaties
Bepaal hoe je JSON eruitziet. Houd het consistent:
snake_caseofcamelCase, maar kies één stijl.- Gebruik duidelijke veldnamen.
- Stuur geen interne database-details mee die je later niet wil ondersteunen.
Voorbeeld van een task response:
{
"id": 42,
"title": "Boodschappen doen",
"description": "Melk, brood, groenten",
"done": false,
"created_at": "2026-02-14T10:15:00Z",
"owner_id": 7
}
HTTP-methodes, statuscodes en idempotentie
Methodes
- GET: ophalen (mag geen server-state wijzigen)
- POST: aanmaken (niet idempotent)
- PUT: volledig vervangen (idempotent)
- PATCH: gedeeltelijk wijzigen (vaak niet strikt idempotent, maar kan)
- DELETE: verwijderen (idempotent in betekenis: meerdere keren verwijderen geeft hetzelfde eindresultaat)
Statuscodes (veelgebruikte set)
200 OK: succesvolle GET/PUT/PATCH/DELETE met response body201 Created: succesvolle POST, liefst metLocationheader204 No Content: succesvolle actie zonder body (bijv. DELETE)400 Bad Request: request is ongeldig (syntactisch of semantisch)401 Unauthorized: niet ingelogd / token ontbreekt of ongeldig403 Forbidden: wel ingelogd, maar geen rechten404 Not Found: resource bestaat niet409 Conflict: conflict (bijv. unieke constraint)422 Unprocessable Entity: validatiefout (FastAPI gebruikt dit vaak)500 Internal Server Error: onverwachte fout
Idempotentie in de praktijk
Idempotentie is belangrijk voor retries. Als een client een request opnieuw verstuurt door timeouts, wil je geen dubbele records.
POST /taskskan dubbel aanmaken als je niet oplet.- Oplossing: idempotency keys (header) of een unieke client-generated identifier.
Datamodellen en validatie met Pydantic
FastAPI gebruikt Pydantic voor:
- parsing van input
- type-validatie
- automatische OpenAPI-schema’s
Je definieert aparte modellen voor:
- Create: velden die client mag sturen bij aanmaken
- Update: optionele velden voor patchen
- Read: velden die je terugstuurt (incl.
id,created_at)
Belangrijk: scheid je API-schema’s van je database-modellen. Dit voorkomt dat databasewijzigingen je API breken en maakt security eenvoudiger (bijv. geen wachtwoordhash terugsturen).
Projectstructuur en afhankelijkheden
We bouwen een minimalistische maar schaalbare structuur.
Vereisten
- Python 3.11+ (werkt ook met 3.10, maar 3.11 is fijn)
- Een virtuele omgeving
- SQLite voor lokaal (later eenvoudig te vervangen door PostgreSQL)
Project aanmaken
mkdir tasks-api
cd tasks-api
python -m venv .venv
source .venv/bin/activate
Installeer dependencies:
pip install fastapi uvicorn sqlalchemy alembic pydantic python-jose passlib[bcrypt] python-multipart pytest httpx
Maak mappen:
mkdir -p app/api app/core app/db app/models app/schemas app/services tests
touch app/__init__.py
Een mogelijke structuur:
app/
api/
routes.py
deps.py
core/
config.py
security.py
logging.py
db/
session.py
base.py
models/
task.py
user.py
schemas/
task.py
user.py
token.py
services/
task_service.py
user_service.py
main.py
tests/
test_tasks.py
Implementatie met FastAPI
main.py
from fastapi import FastAPI
from app.api.routes import router as api_router
app = FastAPI(
title="Tasks API",
version="1.0.0",
)
app.include_router(api_router, prefix="/api")
Router: app/api/routes.py
from fastapi import APIRouter
from app.api import tasks, auth
router = APIRouter()
router.include_router(auth.router, prefix="/auth", tags=["auth"])
router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
We moeten nu app/api/tasks.py en app/api/auth.py maken (zie verderop).
Start de server:
uvicorn main:app --reload --port 8000
Open:
- Documentatie:
http://127.0.0.1:8000/docs - OpenAPI JSON:
http://127.0.0.1:8000/openapi.json
Database-laag met SQLAlchemy en migraties
Database sessie: app/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite:///./app.db"
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False}, # nodig voor SQLite + threads
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base declaratie: app/db/base.py
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
Modellen
app/models/user.py
from sqlalchemy import String, Integer
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String, nullable=False)
app/models/task.py
from datetime import datetime, timezone
from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
title: Mapped[str] = mapped_column(String, nullable=False)
description: Mapped[str] = mapped_column(String, nullable=True)
done: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
)
owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
Tabellen aanmaken (snelle start)
Voor een snelle demo kun je tabellen aanmaken bij start. In productie gebruik je migraties.
Maak app/db/init_db.py:
from app.db.base import Base
from app.db.session import engine
from app.models.user import User
from app.models.task import Task
def init_db() -> None:
Base.metadata.create_all(bind=engine)
Pas main.py aan:
from fastapi import FastAPI
from app.api.routes import router as api_router
from app.db.init_db import init_db
app = FastAPI(title="Tasks API", version="1.0.0")
app.include_router(api_router, prefix="/api")
@app.on_event("startup")
def on_startup() -> None:
init_db()
CRUD-endpoints bouwen
Pydantic schema’s
app/schemas/task.py
from datetime import datetime
from pydantic import BaseModel, Field
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=2000)
class TaskUpdate(BaseModel):
title: str | None = Field(default=None, min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=2000)
done: bool | None = None
class TaskRead(BaseModel):
id: int
title: str
description: str | None
done: bool
created_at: datetime
owner_id: int
class Config:
from_attributes = True
Dependency voor DB-sessie
app/api/deps.py:
from collections.abc import Generator
from app.db.session import SessionLocal
def get_db() -> Generator:
db = SessionLocal()
try:
yield db
finally:
db.close()
Service-laag (optioneel maar nuttig)
Door database-logica buiten je routes te houden, worden je endpoints dunner en beter testbaar.
app/services/task_service.py:
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.models.task import Task
from app.schemas.task import TaskCreate, TaskUpdate
def create_task(db: Session, owner_id: int, data: TaskCreate) -> Task:
task = Task(
title=data.title,
description=data.description,
done=False,
owner_id=owner_id,
)
db.add(task)
db.commit()
db.refresh(task)
return task
def get_task(db: Session, task_id: int, owner_id: int) -> Task | None:
stmt = select(Task).where(Task.id == task_id, Task.owner_id == owner_id)
return db.scalar(stmt)
def list_tasks(db: Session, owner_id: int, limit: int, offset: int) -> list[Task]:
stmt = select(Task).where(Task.owner_id == owner_id).limit(limit).offset(offset)
return list(db.scalars(stmt).all())
def update_task(db: Session, task: Task, data: TaskUpdate) -> Task:
if data.title is not None:
task.title = data.title
if data.description is not None:
task.description = data.description
if data.done is not None:
task.done = data.done
db.commit()
db.refresh(task)
return task
def delete_task(db: Session, task: Task) -> None:
db.delete(task)
db.commit()
Foutenafhandeling en consistente responses
Een veelgemaakte fout is willekeurige error-JSON terugsturen. Beter is:
- consistent format
- duidelijke messages
- juiste statuscodes
FastAPI heeft HTTPException. Voorbeeld:
404als taak niet bestaat403als taak niet van jou is (in ons geval filteren we al opowner_id, dus wordt het404)
Authenticatie en autorisatie (JWT)
We implementeren:
- registreren (email + wachtwoord)
- inloggen (token)
- beveiligde endpoints met
Authorization: Bearer <token>
Security helpers
app/core/security.py:
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
JWT_SECRET = "vervang-dit-door-een-sterk-geheim"
JWT_ALG = "HS256"
JWT_EXPIRE_MINUTES = 60
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
return pwd_context.verify(password, password_hash)
def create_access_token(subject: str) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": subject,
"iat": int(now.timestamp()),
"exp": int((now + timedelta(minutes=JWT_EXPIRE_MINUTES)).timestamp()),
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALG)
def decode_token(token: str) -> str:
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALG])
sub = payload.get("sub")
if not sub:
raise ValueError("Token mist subject")
return str(sub)
except (JWTError, ValueError) as exc:
raise ValueError("Ongeldig token") from exc
User schema’s
app/schemas/user.py:
from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
email: EmailStr
password: str = Field(min_length=8, max_length=200)
class UserRead(BaseModel):
id: int
email: EmailStr
class Config:
from_attributes = True
app/schemas/token.py:
from pydantic import BaseModel
class TokenRead(BaseModel):
access_token: str
token_type: str = "bearer"
User service
app/services/user_service.py:
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.models.user import User
from app.core.security import hash_password, verify_password
def get_user_by_email(db: Session, email: str) -> User | None:
stmt = select(User).where(User.email == email)
return db.scalar(stmt)
def create_user(db: Session, email: str, password: str) -> User:
user = User(email=email, password_hash=hash_password(password))
db.add(user)
db.commit()
db.refresh(user)
return user
def authenticate(db: Session, email: str, password: str) -> User | None:
user = get_user_by_email(db, email)
if not user:
return None
if not verify_password(password, user.password_hash):
return None
return user
Auth routes
app/api/auth.py:
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.schemas.user import UserCreate, UserRead
from app.schemas.token import TokenRead
from app.services.user_service import create_user, get_user_by_email, authenticate
from app.core.security import create_access_token
router = APIRouter()
@router.post("/register", response_model=UserRead, status_code=status.HTTP_201_CREATED)
def register(data: UserCreate, db: Session = Depends(get_db)) -> UserRead:
existing = get_user_by_email(db, data.email)
if existing:
raise HTTPException(status_code=409, detail="E-mail is al geregistreerd")
user = create_user(db, data.email, data.password)
return user
@router.post("/token", response_model=TokenRead)
def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)) -> TokenRead:
user = authenticate(db, form.username, form.password)
if not user:
raise HTTPException(status_code=401, detail="Onjuiste inloggegevens")
token = create_access_token(subject=str(user.id))
return TokenRead(access_token=token)
Let op: OAuth2PasswordRequestForm verwacht application/x-www-form-urlencoded met velden username en password. Dat is standaard voor token endpoints.
Huidige gebruiker dependency
app/api/deps_auth.py:
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.api.deps import get_db
from app.core.security import decode_token
from app.models.user import User
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)) -> User:
try:
user_id = int(decode_token(token))
except ValueError:
raise HTTPException(status_code=401, detail="Ongeldig token")
stmt = select(User).where(User.id == user_id)
user = db.scalar(stmt)
if not user:
raise HTTPException(status_code=401, detail="Gebruiker niet gevonden")
return user
Tasks routes (beveiligd)
app/api/tasks.py:
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.api.deps_auth import get_current_user
from app.models.user import User
from app.schemas.task import TaskCreate, TaskRead, TaskUpdate
from app.services.task_service import create_task, get_task, list_tasks, update_task, delete_task
router = APIRouter()
@router.get("", response_model=list[TaskRead])
def get_tasks(
limit: int = 50,
offset: int = 0,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> list[TaskRead]:
limit = min(max(limit, 1), 200)
offset = max(offset, 0)
return list_tasks(db, owner_id=user.id, limit=limit, offset=offset)
@router.post("", response_model=TaskRead, status_code=status.HTTP_201_CREATED)
def create_new_task(
data: TaskCreate,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> TaskRead:
return create_task(db, owner_id=user.id, data=data)
@router.get("/{task_id}", response_model=TaskRead)
def get_one_task(
task_id: int,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> TaskRead:
task = get_task(db, task_id=task_id, owner_id=user.id)
if not task:
raise HTTPException(status_code=404, detail="Taak niet gevonden")
return task
@router.patch("/{task_id}", response_model=TaskRead)
def patch_task(
task_id: int,
data: TaskUpdate,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> TaskRead:
task = get_task(db, task_id=task_id, owner_id=user.id)
if not task:
raise HTTPException(status_code=404, detail="Taak niet gevonden")
return update_task(db, task, data)
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
def remove_task(
task_id: int,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> None:
task = get_task(db, task_id=task_id, owner_id=user.id)
if not task:
raise HTTPException(status_code=404, detail="Taak niet gevonden")
delete_task(db, task)
return None
Handmatige tests met echte commando’s
Registreren
curl -i -X POST "http://127.0.0.1:8000/api/auth/register" \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"supergeheim123"}'
Token ophalen
curl -i -X POST "http://127.0.0.1:8000/api/auth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=alice@example.com&password=supergeheim123"
Je krijgt JSON met access_token. Zet die in een shell-variabele:
TOKEN="plak-hier-je-token"
Taak aanmaken
curl -i -X POST "http://127.0.0.1:8000/api/tasks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Boodschappen doen","description":"Melk en brood"}'
Takenlijst ophalen
curl -i "http://127.0.0.1:8000/api/tasks?limit=20&offset=0" \
-H "Authorization: Bearer $TOKEN"
Taak bijwerken
curl -i -X PATCH "http://127.0.0.1:8000/api/tasks/1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"done":true}'
Taak verwijderen
curl -i -X DELETE "http://127.0.0.1:8000/api/tasks/1" \
-H "Authorization: Bearer $TOKEN"
Paginatie, filtering en sortering
Zodra je dataset groeit, wil je niet “alles” teruggeven. We gebruikten al limit en offset. Voor grote tabellen is offset soms traag; dan is keyset pagination beter (bijv. “geef items met id > X”). Voor nu is limit/offset prima.
Filtering
Je kunt queryparameters toevoegen zoals:
done=trueq=boodschappen(zoekterm)
Je service-laag kan conditioneel filters toevoegen. Conceptueel:
- bouw een
select(Task)statement - voeg
where(...)toe als parameter aanwezig is - voeg
order_by(...)toe voor sortering
Belangrijk: valideer en whitelist sorteer-velden om SQL-injectie via veldnamen te voorkomen.
Versiebeheer van je API
Er zijn meerdere strategieën:
- URL-versies:
/api/v1/tasks - Header-versies:
Accept: application/vnd... - Query-versies:
?version=1(minder gebruikelijk)
URL-versies zijn het meest praktisch. Je kunt routers onderbrengen in app/api/v1/... en later v2 toevoegen.
Voorbeeld:
app.include_router(api_router, prefix="/api/v1")
Versies zijn vooral nuttig als je breaking changes verwacht. Kleine uitbreidingen (extra velden) kunnen vaak binnen dezelfde versie zolang je backward compatible blijft.
OpenAPI-documentatie en voorbeelden
FastAPI genereert OpenAPI automatisch, maar je kunt het verbeteren met:
- duidelijke
summaryendescription - response-voorbeelden
- tags en grouping
Voorbeeld van extra metadata:
@router.post(
"",
response_model=TaskRead,
status_code=201,
summary="Maak een nieuwe taak aan",
description="Maakt een taak aan voor de ingelogde gebruiker.",
)
def create_new_task(...):
...
Zorg dat je documentatie klopt met je echte gedrag (statuscodes, foutmeldingen). Automatische docs zijn geen excuus om niet na te denken over API-contracten.
Testen met pytest
Testen van API’s is het effectiefst op HTTP-niveau: je test routes, dependencies, validatie en auth in één.
Maak tests/test_tasks.py (vereenvoudigd voorbeeld). Let op: voor nette tests wil je een aparte testdatabase en dependency overrides. Hieronder een basispatroon met TestClient.
Installeer testclient dependency (FastAPI gebruikt Starlette):
pip install pytest
Voorbeeldtest (conceptueel; voor echte isolatie moet je DB override toevoegen):
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_register_and_create_task():
r = client.post("/api/auth/register", json={"email": "bob@example.com", "password": "wachtwoord123"})
assert r.status_code in (201, 409)
token_resp = client.post(
"/api/auth/token",
data={"username": "bob@example.com", "password": "wachtwoord123"},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
assert token_resp.status_code == 200
token = token_resp.json()["access_token"]
task_resp = client.post(
"/api/tasks",
json={"title": "Test taak", "description": "Via pytest"},
headers={"Authorization": f"Bearer {token}"},
)
assert task_resp.status_code == 201
body = task_resp.json()
assert body["title"] == "Test taak"
assert body["done"] is False
Voor productiekwaliteit tests:
- gebruik een tijdelijke SQLite in-memory of een aparte testfile
- override
get_db()zodat elke test in een transactie draait en rollbackt - test ook foutpaden: 401 zonder token, 404 voor onbekende id, 422 voor invalid payload
Logging, configuratie en omgevingsvariabelen
Hardcoded secrets zoals JWT_SECRET zijn onveilig. Gebruik omgevingsvariabelen.
Een eenvoudige configuratie: app/core/config.py
import os
def get_env(name: str, default: str | None = None) -> str:
value = os.getenv(name, default)
if value is None:
raise RuntimeError(f"Ontbrekende omgevingsvariabele: {name}")
return value
JWT_SECRET = get_env("JWT_SECRET", "dev-only-vervang-dit")
DATABASE_URL = get_env("DATABASE_URL", "sqlite:///./app.db")
Pas dan session.py en security.py aan om deze waarden te gebruiken.
Logging: begin simpel en structureer later. Uvicorn logt al requests, maar applicatielogs zijn ook nuttig:
- authenticatiepogingen (zonder wachtwoorden te loggen)
- databasefouten
- onverwachte exceptions
Deployment met Docker
Docker maakt je runtime reproduceerbaar.
Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY . /app
ENV PYTHONUNBUFFERED=1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Maak requirements.txt:
pip freeze > requirements.txt
Build en run:
docker build -t tasks-api:latest .
docker run --rm -p 8000:8000 -e JWT_SECRET="een-sterk-geheim" tasks-api:latest
Voor productie:
- zet
--reloaduit - gebruik een process manager of meerdere workers, bijvoorbeeld:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2 - gebruik PostgreSQL in plaats van SQLite
Checklist voor productie
API-contract en ontwerp
- Gebruik consistente naming en response-structuren
- Juiste statuscodes
- Paginatie op collecties
- Duidelijke foutmeldingen
Security
- Secrets via omgevingsvariabelen
- Sterke wachtwoordhashing (bcrypt of beter)
- JWT met korte expiratie en eventueel refresh tokens
- Rate limiting (bijv. via reverse proxy)
- CORS correct configureren als je browserclients hebt
Database
- Migraties (Alembic) in plaats van
create_all - Indexen op veelgebruikte velden
- Unieke constraints waar nodig
- Transacties en foutafhandeling
Observability
- Logging met request-id
- Metrics (latency, error rates)
- Tracing indien nodig
Tests
- Unit tests voor services
- Integratietests voor endpoints
- Tests voor auth en autorisatie
- Tests voor validatie en foutpaden
Volgende stappen
Als je deze basis hebt staan, zijn logische uitbreidingen:
- Alembic migraties toevoegen en een echte
DATABASE_URLvoor PostgreSQL - Refresh tokens en token revocation
- Rollen en permissies (bijv. admin vs user)
- ETags en caching headers voor efficiëntere GET’s
- Webhooks of async background tasks (bijv. met Celery of built-in background tasks)
Als je wil, kan ik je project omzetten naar een volledige v1-structuur met Alembic migraties en PostgreSQL, inclusief dependency overrides voor nette tests.