Ga naar hoofdinhoud

Cheatsheet

Hier zoek je op hoe iets uit de lessen ook alweer ging. De onderwerpen staan in de volgorde van de lessen. Zoek met het veld hieronder, of klik een onderwerp aan om het te openen. Welk bestand in welke map hoort, staat bij Projectstructuur; alles over de database bij SqliteDict op een rij.

0 onderwerpen. Typ / om te zoeken.

FastAPI​

Hoe installeer ik FastAPI? (python -m pip install)

Typ dit in de terminal van VS Code, met je virtual environment aan:

python -m pip install "fastapi[standard]"

Hoe je de virtual environment maakt, staat bij Installatie.

Hoe maak ik een FastAPI-app? (FastAPI)
from fastapi import FastAPI

app = FastAPI()

Dit staat één keer in je bestand, direct na de imports, en alle endpoints komen eronder.

Waar haal ik een naam vandaan? (import)

Elke naam die je niet zelf maakt, zet je bovenaan je bestand met een import. Meer namen van dezelfde plek komen in één regel, met komma's ertussen:

NaamRegel bovenaan je bestand
FastAPI, Form, Request, Response, HTTPException, Cookiefrom fastapi import …
HTMLResponse, FileResponse, RedirectResponse, JSONResponsefrom fastapi.responses import …
StaticFilesfrom fastapi.staticfiles import StaticFiles
Jinja2Templatesfrom fastapi.templating import Jinja2Templates
SqliteDictfrom sqlitedict import SqliteDict
escapefrom html import escape

Response zelf staat in fastapi; de soorten antwoorden met een woord ervoor, zoals HTMLResponse, staan in fastapi.responses.

from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse

Staat er in VS Code een kronkel onder een naam, zet je cursor erop en druk op Ctrl+.: vaak stelt VS Code de import-regel dan voor.

Hoe start ik mijn server? (fastapi dev)
fastapi dev main.py

Open http://127.0.0.1:8000. Stoppen doe je met Ctrl+C in de terminal.

Hoe geef ik JSON terug? (@app.get)
@app.get("/")
async def root():
return {"bericht": "Hallo wereld!"}
Hoe geef ik een HTML-pagina terug? (HTMLResponse)
from fastapi.responses import HTMLResponse

@app.get("/pagina", response_class=HTMLResponse)
async def pagina():
return """
<!DOCTYPE html>
<html>
<body><h1>Hallo</h1></body>
</html>
"""
Hoe stuur ik een HTML-bestand? (FileResponse)
from fastapi.responses import FileResponse

@app.get("/")
async def home():
return FileResponse("static/pages/home.html")
Hoe maak ik CSS en afbeeldingen bereikbaar? (app.mount)
from fastapi.staticfiles import StaticFiles

app.mount("/static", StaticFiles(directory="static"), name="static")

Alles in de map static staat dan op /static/....

Hoe vul ik een template in? (TemplateResponse)
from fastapi import Request
from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory="templates")

@app.get("/dobbelsteen")
async def dobbelsteen(request: Request):
return templates.TemplateResponse(request, "dobbelsteen.html", {"worp": 4})

request komt als eerste argument, vóór de bestandsnaam.

Hoe lees ik iets na het vraagteken in de URL? (query-parameter)
@app.get("/zoek")
async def zoek(term: str = ""):
return {"je_zocht": term}

/zoek?term=python geeft term de waarde python. Met de standaardwaarde "" werkt /zoek ook zonder vraagteken; zonder standaardwaarde krijg je dan een 422.

Hoe ontvang ik een formulier? (@app.post en Form)
from fastapi import Form

@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
return {"naam": naam, "bericht": bericht}

De name in je HTML moet precies zo heten als de parameter in Python.

Hoe stuur ik door na een POST? (RedirectResponse)
from fastapi.responses import RedirectResponse

@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...)):
return RedirectResponse(url="/berichten", status_code=303)

Zonder status_code=303 krijg je een 405: de standaard is 307, en die herhaalt je POST.

Hoe haal ik een waarde uit het pad? (path-parameter)
@app.get("/bericht/{sleutel}")
async def bericht_detail(sleutel: str):
return {"sleutel": sleutel}

De naam tussen de accolades moet gelijk zijn aan de parameternaam.

Hoe stuur ik een 404 als iets niet bestaat? (HTTPException)
from fastapi import HTTPException

@app.get("/bericht/{sleutel}")
async def bericht_detail(sleutel: str):
with SqliteDict("gastenboek.db") as db:
bericht = db.get(sleutel)
if bericht is None:
raise HTTPException(status_code=404, detail="Dit bericht bestaat niet")
return bericht

Met raise, niet met return.

Hoe maak ik een account? (registreren)
@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as gebruikers:
if naam in gebruikers:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
gebruikers[naam] = wachtwoord
gebruikers.commit()
return RedirectResponse(url="/gastenboek", status_code=303)

Zonder de controle op een bestaande naam neemt een tweede registratie het account over. Het wachtwoord staat zo nog leesbaar in gebruikers.db; hoe je het veilig bewaart, staat onder Veiligheid.

Hoe controleer ik een wachtwoord? (inloggen met 401)
with SqliteDict("gebruikers.db") as gebruikers:
opgeslagen = gebruikers.get(naam)
if opgeslagen != wachtwoord:
raise HTTPException(status_code=401, detail="Naam of wachtwoord klopt niet")

Een naam zonder account geeft None, en dat is nooit gelijk aan een wachtwoord. Geef bij een onbekende naam en een fout wachtwoord dezelfde melding, anders verraad je welke namen een account hebben.

Hoe weiger ik invoer die niet klopt? (HTTPException met 400)
@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
if len(bericht) > 80:
raise HTTPException(status_code=400, detail="Bericht is te lang")
return RedirectResponse(url="/berichten", status_code=303)

Controleer aan het begin van je endpoint, vóór je iets opslaat. Een maxlength in de HTML is geen controle: die kan een bezoeker weghalen.

Wat betekent deze statuscode?
CodeBetekentWie stuurt hem
200geluktje endpoint
303ga naar deze URL, met een GETjij, met RedirectResponse(..., status_code=303)
307doe hetzelfde verzoek op deze URLRedirectResponse zonder status_code
400dit verzoek klopt nietjij, met HTTPException
401je bent niet (goed) ingelogdjij, met HTTPException
403dit mag jij nietjij, bij iets van een ander
404bestaat nietFastAPI of jij
405dit pad bestaat, maar niet voor deze soort verzoekFastAPI
422een waarde ontbreekt of past nietFastAPI, vóór je functie
429te veel verzoekenslowapi
500fout in je serverniemand bewust; kijk in de terminal

400, 403 en 429 komen terug in Veiligheid.

Hoe zet ik de tijd in een antwoord? (datetime.now)
from datetime import datetime

@app.get("/tijd")
async def tijd():
return HTMLResponse(f"Het is nu {datetime.now().strftime('%H:%M:%S')}")

strftime('%H:%M') geeft alleen uren en minuten, zoals 14:32.

Hoe verwijder ik zonder herladen? (@app.delete)
from fastapi.responses import HTMLResponse

@app.delete("/bericht/{sleutel}")
async def bericht_verwijderen(sleutel: str):
with SqliteDict("gastenboek.db") as db:
if sleutel not in db:
raise HTTPException(status_code=404, detail="Dit bericht bestaat niet")
del db[sleutel]
db.commit()
return HTMLResponse("")

Een formulier zonder htmx kan alleen GET en POST; hx-delete stuurt een DELETE. Met hx-swap="outerHTML" haalt het lege antwoord het doel van de pagina.

Hoe geef ik een cookie mee? (set_cookie)
@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...)):
antwoord = RedirectResponse(url="/berichten", status_code=303)
antwoord.set_cookie(key="naam", value=naam, max_age=60 * 60 * 24 * 30)
return antwoord

Maak het antwoord eerst als variabele, anders heb je geen plek om de cookie op te zetten. max_age is de houdbaarheid in seconden.

Hoe lees ik een cookie uit? (Cookie)
from fastapi import Cookie

@app.get("/gastenboek")
async def gastenboek_form(request: Request, naam: str = Cookie(default="")):
return templates.TemplateResponse(request, "gastenboek.html", {"naam": naam})

Weghalen doe je met antwoord.delete_cookie("naam").

Een cookie staat bij de bezoeker, en die kan hem veranderen. Gebruik hem dus niet voor iets waar rechten aan hangen.

Hoe onthoud ik wie er is ingelogd? (sessie met secrets)
import secrets

@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as gebruikers:
opgeslagen = gebruikers.get(naam)
if opgeslagen != wachtwoord:
raise HTTPException(status_code=401, detail="Naam of wachtwoord klopt niet")

sessie_id = secrets.token_hex(16)
with SqliteDict("sessies.db") as sessies:
sessies[sessie_id] = {"naam": naam}
sessies.commit()

antwoord = RedirectResponse(url="/gastenboek", status_code=303)
antwoord.set_cookie(key="sessie_id", value=sessie_id, max_age=60 * 60 * 24 * 30)
return antwoord

Uitlezen, in elk endpoint met de parameter sessie_id: str = Cookie(default=""):

with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})

In de cookie staat alleen het sessie-id; wie er inlogde, staat op de server. Een lege dictionary betekent: niet ingelogd. Zet de naam bij een bericht met mijn["naam"], nooit uit een formulierveld.

Hoe maak ik een eigen 404-pagina? (exception_handler)
@app.exception_handler(404)
async def niet_gevonden(request: Request, fout):
return templates.TemplateResponse(
request,
"404.html",
{"pad": request.url.path},
status_code=404,
)

Deze handler krijgt elke 404: een adres zonder endpoint en je eigen raise HTTPException(status_code=404, …). Andere fouten blijven zoals ze waren, en een fout in je code blijft een 500. Zet geen handler op Exception: dan lijkt een fout in je code een ontbrekende pagina.

Hoe zet ik mijn server open voor het netwerk? (--host 0.0.0.0)
fastapi dev main.py --host 0.0.0.0

Zoek je adres op en geef http://<jouw-adres>:8000 door:

# Windows: de regel IPv4-adres
ipconfig

# macOS
ipconfig getifaddr en0

# Linux
ip addr

Let op: iedereen op hetzelfde netwerk kan er dan bij.

Browser​

Hoe open ik de ontwikkelaarstools? (F12)

Druk op F12, of klik met de rechtermuisknop op de pagina en kies Inspecteren. Dat laatste opent meteen het tabblad Elementen op het element waar je op klikte.

De tabbladen die je in deze cursus gebruikt:

  • Console: fouten uit je JavaScript, met bestand en regelnummer
  • Netwerk: elk verzoek van de pagina, met statuscode en herkomst
  • App (Application; in Firefox Opslag): de cookies van de site
Hoe zie ik welke verzoeken de pagina doet? (tabblad Netwerk)

Open het tabblad Netwerk en herlaad de pagina. Elk verzoek krijgt een eigen regel.

  • Status 200: gevonden. 404: het pad klopt niet, of app.mount ontbreekt.
  • Grootte met (schijfcache): de browser heeft de server niet gevraagd.
  • Klik een regel aan voor de headers, zoals Cookie: bij een sessie.
Hoe herlaad ik zonder cache? (Ctrl+Shift+R)

Met de ontwikkelaarstools open: rechtermuisknop op de herlaadknop, dan Cache wissen en geforceerd opnieuw laden (Empty Cache and Hard Reload). Sneltoets: Ctrl+Shift+R.

Zet zolang je aan je site werkt in het tabblad Netwerk het vinkje Cache uitzetten aan.

HTML​

Hoe ziet een HTML-pagina eruit? (DOCTYPE)
<!DOCTYPE html>
<html>
<head>
<title>Titel</title>
</head>
<body>
<h1>Kop</h1>
<p>Alinea</p>
</body>
</html>
Hoe link ik naar een andere pagina? (a href)
<a href="/about">Over mij</a>

Link naar het endpoint, niet naar het bestand.

Hoe koppel ik CSS aan mijn pagina? (link)
<link rel="stylesheet" href="/static/css/style.css">
Hoe toon ik een afbeelding? (img)
<img src="/static/kat.jpg" alt="Een kat op een vensterbank">
Hoe zet ik een waarde uit Python in een template? (dubbele accolades)

In Python:

return templates.TemplateResponse(request, "welkom.html", {"naam": "Sam", "speler": {"punten": 12}})

In de template:

<h1>Welkom {{ naam }}</h1>
<p>{{ speler.punten }} punten</p>

De naam tussen de accolades is de sleutel uit het dictionary. Een onderdeel van een dictionary haal je eruit met een punt.

Hoe maak ik een formulier? (form method="post")
<form method="post" action="/gastenboek">
<input type="text" name="naam" required>
<button type="submit">Verstuur</button>
</form>

De name van een veld is de naam van de parameter in Python.

Hoe herhaal ik iets voor elk bericht? (for-lus in een template)
<ul>
{% for bericht in berichten %}
<li>{{ bericht.naam }}: {{ bericht.bericht }}</li>
{% endfor %}
</ul>

Binnen de lus telt {{ loop.index }} vanaf 1.

Hoe vang ik een lege lijst op? (if en else in een template)
{% if berichten %}
<p>Er zijn berichten</p>
{% else %}
<p>Nog geen berichten</p>
{% endif %}
Hoe maak ik een verwijderknop per bericht? (verborgen veld)

Stuur de berichten mét sleutel mee (list(db.items()), zie Database). In de template:

{% for sleutel, bericht in berichten %}
<li>
{{ bericht.naam }}: {{ bericht.bericht }}
<form method="post" action="/verwijderen">
<input type="hidden" name="sleutel" value="{{ sleutel }}">
<button type="submit">Verwijderen</button>
</form>
</li>
{% endfor %}

In main.py:

@app.post("/verwijderen")
async def verwijderen(sleutel: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
if sleutel in db:
del db[sleutel]
db.commit()
return RedirectResponse(url="/berichten", status_code=303)

Een type="hidden" ziet de bezoeker niet, maar hij gaat wel mee met het formulier.

Hoe link ik naar de pagina van één bericht? (sleutel in de link)
{% for sleutel, bericht in berichten %}
<li><a href="/bericht/{{ sleutel }}">{{ bericht.naam }}</a></li>
{% endfor %}

De link komt uit bij het endpoint /bericht/{sleutel} (zie Hoe haal ik een waarde uit het pad?).

Hoe gebruik ik een stuk template opnieuw? (include)

templates/berichten_lijst.html is een stuk pagina zonder <html> eromheen. In de pagina:

<div id="berichten-lijst">
{% include "berichten_lijst.html" %}
</div>

Hetzelfde bestand geeft je endpoint terug als antwoord op een htmx-verzoek.

Mappenstructuur​

Welk bestand hoort in welke map? (projectstructuur)
je-project/
├── main.py
├── gastenboek.db
├── gebruikers.db
├── sessies.db
├── static/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ ├── app.js
│ │ └── htmx.min.js
│ ├── pages/
│ │ └── home.html
│ └── kat.jpg
└── templates/
├── bericht.html
├── berichten.html
├── berichten_lijst.html
└── gastenboek.html

Vaste pagina's staan in static/pages/, pagina's met {{ }} in templates/. Het gastenboekformulier begint als static/pages/gastenboek_form.html en verhuist bij Onthouden met een cookie naar templates/gastenboek.html; in sessies wordt het een inlogformulier of een formulier om te schrijven. De .db-bestanden maakt sqlitedict zelf aan. Hoe de mappen per les groeien, staat bij Projectstructuur.

Database (sqlitedict)​

Alles over SqliteDict op één pagina, met de punten waarop hij anders is dan een gewone dictionary: SqliteDict op een rij.

Hoe installeer ik sqlitedict? (python -m pip install)
python -m pip install sqlitedict
Hoe sla ik iets op? (db[...] = en commit)
from sqlitedict import SqliteDict

with SqliteDict("data.db") as db:
db["naam"] = "Jan"
db.commit()

Zonder db.commit() is je wijziging weg zodra het with-blok sluit: open je de database daarna opnieuw, ook in hetzelfde programma, dan staat hij er niet in.

Hoe lees ik iets uit? (db[...])
with SqliteDict("data.db") as db:
print(db["naam"])

Bestaat de sleutel niet, dan krijg je een KeyError. Dat gebeurt ook als je script in een andere map draait: daar maakt SqliteDict stil een lege database aan.

Hoe bekijk ik alles wat erin staat? (db.items)
with SqliteDict("data.db") as db:
print(len(db), "sleutels")
for sleutel, waarde in db.items():
print(sleutel, "=", waarde)

Elk element is een paar: eerst de sleutel, dan de waarde. Stuur je ze naar een template, maak er dan binnen het with-blok een lijst van met list(db.items()), en loop in de template met {% for sleutel, bericht in berichten %}.

Hoe kijk ik of een sleutel bestaat? (in)
with SqliteDict("data.db") as db:
if "email" in db:
print(db["email"])
else:
print("Geen email opgeslagen")
Hoe lees ik iets uit dat misschien niet bestaat? (db.get)
with SqliteDict("data.db") as db:
naam = db.get("naam", "Niet gevonden")

Zonder tweede argument geeft db.get() None als de sleutel niet bestaat, in plaats van een KeyError.

Hoe verwijder ik iets? (del db[...])
with SqliteDict("gastenboek.db") as db:
if sleutel in db:
del db[sleutel]
db.commit()

Zonder if sleutel in db crasht je endpoint als iemand twee keer op Verwijderen klikt.

Hoe sla ik meer dan één ding onder een sleutel op? (dictionary als waarde)
with SqliteDict("data.db") as db:
db["sara"] = {"klas": "4B", "vakken": ["informatica", "wiskunde"]}
db.commit()

Uitlezen gaat in twee stappen: db["sara"]["klas"].

Hoe pas ik een opgeslagen dictionary aan? (ophalen, aanpassen, terugzetten)
with SqliteDict("data.db") as db:
sara = db["sara"]
sara["vakken"].append("biologie")
db["sara"] = sara
db.commit()

db["sara"] geeft een kopie. db["sara"]["vakken"].append(...) past alleen die kopie aan, en dan blijft in het bestand de oude waarde staan, zonder foutmelding.

Hoe geef ik elk bericht een eigen sleutel? (time.time_ns)
import time

with SqliteDict("gastenboek.db") as db:
sleutel = f"bericht_{time.time_ns()}"
db[sleutel] = {"naam": "Sam", "bericht": "Hoi"}
db.commit()

Met een vaste sleutel, of met int(time.time()), overschrijft een nieuw bericht het vorige.

Hoe stuur ik alle berichten naar een template? (list met db.values)
@app.get("/berichten")
async def berichten(request: Request):
with SqliteDict("gastenboek.db") as db:
alle_berichten = list(db.values())
return templates.TemplateResponse(request, "berichten.html", {"berichten": alle_berichten})

list() haalt de berichten op zolang de database nog open is. Zonder list() krijg je AttributeError: 'NoneType' object has no attribute 'select'.

htmx​

Hoe koppel ik htmx aan mijn pagina? (htmx.min.js)

Download htmx.min.js (de link staat bij Zonder herladen met htmx) naar static/js/, en zet deze regel in de <head> van elke pagina die htmx gebruikt:

<script src="/static/js/htmx.min.js"></script>
Hoe stuur ik een verzoek zonder herladen? (hx-post, hx-get, hx-delete)
<form hx-post="/gastenboek" hx-target="#antwoord">
<p id="antwoord"></p>
<button hx-get="/berichten/lijst" hx-target="#berichten-lijst">Ververs</button>
<button hx-delete="/bericht/{{ sleutel }}" hx-target="#bericht-{{ sleutel }}" hx-swap="outerHTML">Verwijderen</button>

Het endpoint geeft een stukje HTML terug (HTMLResponse("Bedankt") of een template zonder <html> eromheen), nooit een omleiding. Het element uit hx-target moet op dezelfde pagina staan als de knop of het formulier; bestaat het niet, dan stuurt htmx geen verzoek.

Waar komt het antwoord terecht? (hx-target en hx-swap)
  • hx-target="#id": het element dat het antwoord krijgt. Zonder hx-target is dat het element met het attribuut zelf.
  • hx-swap="innerHTML" (standaard): vervangt wat er in het doel staat.
  • hx-swap="outerHTML": vervangt het doel zelf. Met een leeg antwoord verdwijnt het.
  • hx-swap="beforeend": zet het antwoord achter wat er al in het doel staat.
Wanneer gaat het verzoek? (hx-trigger)
<div id="berichten-lijst" hx-get="/berichten/lijst" hx-trigger="every 10s">

<input type="search" name="term" hx-get="/berichten/lijst" hx-target="#berichten-lijst"
hx-trigger="keyup changed delay:300ms">

Zonder hx-trigger gaat het verzoek bij een klik op een knop of als je een formulier verstuurt. Een <input> stuurt zijn waarde mee als query-parameter (/berichten/lijst?term=hoi); delay:300ms wacht tot je even stopt met typen.

Hoe vraag ik eerst om bevestiging? (hx-confirm)
<button hx-delete="/bericht/{{ sleutel }}" hx-confirm="Dit bericht verwijderen?">Verwijderen</button>
Hoe maak ik een formulier leeg na versturen? (hx-on::after-request)
<form hx-post="/gastenboek" hx-target="#antwoord" hx-on::after-request="this.reset()">

Met een Content-Security-Policy op je site werkt dit attribuut niet meer; zie HTML van een bezoeker: in de praktijk.

JavaScript​

Hoe koppel ik JavaScript aan mijn pagina? (script met defer)

Zet je bestand in static/js/app.js en deze regel in de <head> van je template:

<script src="/static/js/app.js" defer></script>

app.mount("/static", ...) serveert het al; aan main.py verandert niets.

Zonder defer draait je script voordat de pagina er staat, en vindt querySelector niets.

Hoe reageer ik op typen of klikken? (addEventListener)
const veld = document.querySelector("#bericht-veld");
const teller = document.querySelector("#teller");

veld.addEventListener("input", function () {
teller.textContent = 80 - veld.value.length + " tekens over";
});
Hoe zie ik wat mijn JavaScript doet? (console.log)
console.log("nog", over, "tekens");

De regel verschijnt in het tabblad Console van de ontwikkelaarstools, met rechts het bestand en het regelnummer. Fouten staan daar in het rood.

Veiligheid​

Hoe zet ik de lijst met endpoints uit? (docs_url=None)
from fastapi import FastAPI

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

Zonder deze instellingen zet FastAPI al je endpoints op /docs, /redoc en /openapi.json, voor iedereen die je server kan bereiken. Uitzetten verstopt alleen: haal een endpoint dat niet voor iedereen is ook echt weg. Zet .venv/, __pycache__/ en *.db in je .gitignore. Zie Wat je server laat zien: de handleiding uitzetten en een .gitignore en een nette zip.

Hoe begrens ik invoer op de server? (Form met max_length)
@app.post("/bericht")
async def bericht_plaatsen(bericht: str = Form(..., max_length=280)):
...

maxlength in de HTML helpt alleen wie zich vergist; de echte grens staat in Form. Zie Invoer controleren: grenzen in Form.

Hoe laat ik een bezoeker een pagina kiezen? (lijst met wat mag)
PAGINAS = ["home.html", "over.html"]

@app.get("/pagina")
async def pagina(naam: str):
if naam not in PAGINAS:
raise HTTPException(status_code=404, detail="Deze pagina bestaat niet")
return FileResponse(f"static/pages/{naam}")

Zonder de lijst vraagt een bezoeker met ?naam=../../main.py om de code van je server. Zie Invoer controleren: een bestandsnaam van een bezoeker.

Hoe maak ik HTML van een bezoeker onschadelijk? (escape)
from html import escape

return HTMLResponse(f"Bedankt, {escape(naam)}.")

Een {{ }}-template escapet vanzelf; een f-string niet. Ook een query-parameter komt van de bezoeker. Gebruik |safe nooit voor tekst van een bezoeker. Zie HTML van een bezoeker: escapen in Python.

Hoe zet ik tekst van een bezoeker in de pagina met JavaScript? (textContent)
voorbeeld.textContent = veld.value;

textContent houdt het tekst; innerHTML leest het als HTML. Gebruik innerHTML alleen voor HTML die je zelf schreef. Zie HTML van een bezoeker: tekst blijft tekst met textContent.

Hoe beperk ik het aantal verzoeken? (slowapi)

Dit is de hele main.py uit de map veiligheid-dos van de reeks Te veel verzoeken: een limiet met slowapi, met een eigen app = FastAPI(). Zet je een limiet in je gastenboek, neem dan de imports en de regels met limiter en app.state en app.add_exception_handler over, en zet @limiter.limit onder de @app.-regel van je eigen endpoint. Geen tweede app = FastAPI(): die maakt je andere endpoints onbereikbaar.

from fastapi import FastAPI, Request, Response
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address, headers_enabled=True)

app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/")
@limiter.limit("5/minute")
async def root(request: Request, response: Response):
return {"bericht": "Hallo wereld!"}

Het endpoint heeft request: Request nodig, en @limiter.limit staat direct onder @app.get of @app.post, niet erboven. Met headers_enabled=True vertelt elk antwoord hoeveel verzoeken er nog over zijn; een endpoint dat een dictionary teruggeeft, heeft dan ook response: Response nodig. Installeer met python -m pip install slowapi. Een limiet op /inloggen en /wijzig staat in Wachtwoorden: inlogpogingen beperken.

Hoe controleer ik wie iets mag? (403)
with SqliteDict("gastenboek.db") as db:
bericht = db.get(sleutel)
if bericht is None or bericht["naam"] != mijn.get("naam"):
raise HTTPException(status_code=403, detail="Dit is niet jouw bericht")

mijn is de sessie uit sessies.db, met de naam waarmee de bezoeker inlogde.

Zet de controle in het endpoint, vóór er iets verandert, en op elk endpoint apart. Zie Wie mag wat: de controle met 403.

Hoe houd ik scripts bij mijn cookie weg? (httponly)
antwoord.set_cookie(
key="sessie_id",
value=sessie_id,
max_age=60 * 60 * 24 * 30,
httponly=True,
samesite="lax",
)

httponly houdt scripts bij de cookie weg. Met samesite="lax", dat FastAPI al vanzelf zet, gaat de cookie niet mee als een formulier op een andere site naar jouw site post; bij een klik op een link daar wel. secure=True (met https) stuurt hem alleen versleuteld. Zie Cookies afschermen: httponly en samesite.

Hoe bewaar ik een wachtwoord veilig? (Argon2)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()

hash = ph.hash("welkom123")

try:
ph.verify(hash, "welkom123")
print("Ingelogd")
except VerifyMismatchError:
print("Klopt niet")

Bewaar de hash, nooit het wachtwoord. Vergelijk met ph.verify en niet zelf met ==: elke hash heeft een eigen zout. Bestaat de naam niet, doe dan toch een verify tegen een nep-hash, zodat het antwoord even lang duurt. Zie Wachtwoorden: registreren met Argon2 en inloggen met verify.

Online zetten​

Hoe maak ik een image van mijn site? (Dockerfile)
FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .
RUN python -m pip install -r requirements.txt

COPY . .

RUN useradd --system app && mkdir data && chown app data
USER app

CMD ["fastapi", "run", "main.py", "--port", "8000"]

Bouwen doe je met docker build -t gastenboek .. Zet .venv, __pycache__, .git en *.db in .dockerignore, en elke database in data/. Zie Online zetten: je site in een image en niet als root.

Hoe start en stop ik mijn site? (docker compose)
docker compose up -d --build
docker compose ps
docker compose logs --tail 20
docker compose stop

up -d --build bouwt het image en start de container op de achtergrond. ps laat zien wat draait, logs wat je site printte. Zie Online zetten: alles in één bestand.

Wat blijft er bewaard? (rm, down en een volume)
CommandoContainerWat erin staatVolume
docker compose stopblijftblijftblijft
docker rm -f gastenboekwegwegblijft
docker compose downwegwegblijft
een nieuwe versie met --buildnieuwwegblijft
docker compose down -vwegwegweg

Wat je site in een volume schrijft, overleeft een nieuwe container; de rest niet. Typ down -v nooit op een server. Zie Online zetten: je berichten bewaren.

Hoe zet ik een nieuwe versie op de server? (git pull en --build)
cd ~/gastenboek
git pull
docker compose up -d --build

Verander je code op je laptop, push hem, en haal hem op de server op. Met docker compose ps zie je daarna of je site (healthy) is. Zie Online zetten: een nieuwe versie uitrollen.

Hoe maak ik een back-up? (docker compose cp)
mkdir ~/backup
cd ~/gastenboek
docker compose stop
docker compose cp gastenboek:/app/data ~/backup/data
docker compose start

Zet .env en ~/traefik erbij, en haal de map met scp -r naar je laptop. Terugzetten eindigt met docker compose exec -u root gastenboek chown -R app /app/data. Zie Online zetten: een back-up en een back-up terugzetten.

Hoe werk ik Python en Traefik bij? (--pull)
cd ~/gastenboek
docker compose build --pull
docker compose up -d
cd ~/traefik
docker compose pull
docker compose up -d

Zonder --pull bouwt Docker op het Python-image dat al op de server staat. Ruim daarna op met docker image prune. Zie Online zetten: bijwerken en opruimen.