Ga naar hoofdinhoud

Er gaat iets mis

Klik op je probleem om de oplossing te zien.

Server starten

fastapi: command not found

Oorzaak: FastAPI is niet geïnstalleerd in de omgeving waar je terminal nu in werkt.

Oplossing: check eerst of je (.venv) vooraan je terminalregel ziet. Installeer daarna:

python -m pip install "fastapi[standard]"

Meer uitleg: Installatie

ModuleNotFoundError: No module named 'fastapi'

Oorzaak: je virtual environment is niet actief, dus Python kijkt in de verkeerde map naar geïnstalleerde pakketten.

Oplossing: open je project in VS Code met code . vanuit je projectmap en werk in de terminal van VS Code (TerminalNew Terminal); die zet de venv zelf aan. Lukt dat niet, activeer hem dan met de hand en check dat (.venv) in je terminal verschijnt:

# Windows
.venv\Scripts\activate

# Mac/Linux
source .venv/bin/activate

Meer uitleg: Installatie

Address already in use (poort 8000 bezet)

Oorzaak: er draait al een server op poort 8000 — meestal een vorige fastapi dev in een andere terminal die je vergeten bent.

Oplossing: sluit die andere terminal (of stop hem met Ctrl+C), of start op een andere poort:

fastapi dev main.py --port 8001

Meer uitleg: Je eerste endpoint

Server start maar pagina laadt niet

Oorzaak: de browser praat niet met de server die je net startte — verkeerd adres, of de server is inmiddels gestopt.

Oplossing:

  1. Check of je naar http://127.0.0.1:8000 gaat (niet https)
  2. Check of de server nog draait in de terminal
  3. Herlaad zonder cache (Ctrl+Shift+R, zie Wijzigingen zijn niet zichtbaar)

Meer uitleg: Je eerste endpoint

Mijn klasgenoot kan niet bij mijn server

Oorzaak: je server luistert alleen naar je eigen computer, of jullie zitten niet op hetzelfde netwerk.

Oplossing: check in deze volgorde:

  1. Draait je server met --host 0.0.0.0? Zonder dat luistert hij alleen naar je eigen computer.
  2. Geef je het juiste adres door? 127.0.0.1 verwijst bij hem naar zijn eigen computer, niet naar die van jou. Zoek je adres met ipconfig of ip addr.
  3. Zitten jullie op hetzelfde netwerk? Het gastennetwerk op school staat vaak los van het schoolnetwerk.
  4. Vraagt je firewall om toestemming? Die moet je toestaan.

Meer uitleg: Laat het aan anderen zien

HTML & CSS

CSS werkt niet / styling is weg

Oorzaak: de browser kan het CSS-bestand niet ophalen — de static-map is niet gekoppeld, of het pad in je <link> wijst ernaast.

Oplossing:

  1. Staat app.mount("/static", StaticFiles(directory="static"), name="static") in je code?
  2. Staat je CSS-bestand in static/css/style.css?
  3. Staat in je HTML: <link rel="stylesheet" href="/static/css/style.css">?
  4. Herstart de server en herlaad zonder cache (Ctrl+Shift+R, zie Wijzigingen zijn niet zichtbaar)

Open http://127.0.0.1:8000/static/css/style.css rechtstreeks: zie je je CSS, dan ligt het aan de <link>; een 404, dan aan het pad of de mount.

Meer uitleg: Static files

404 Not Found bij het openen van een pagina

Oorzaak: het endpoint bestaat niet. De URL die je opvraagt komt met geen enkele @app.get(...) in je main.py overeen — een typfout in de link, of het endpoint is nooit gemaakt.

Oplossing: vergelijk de URL in de adresbalk letter voor letter met het pad in je decorator. Een veelgemaakte variant is linken naar het bestand in plaats van het endpoint:

<!-- FOUT - de browser zoekt een endpoint /about.html, dat bestaat niet -->
<a href="about.html">Over mij</a>

<!-- GOED - het endpoint uit je main.py -->
<a href="/about">Over mij</a>

Meer uitleg: Links tussen pagina's

500 Internal Server Error, en in de terminal: RuntimeError ... does not exist
RuntimeError: File at path static/pages/home.html does not exist.

Oorzaak: het endpoint bestaat wél, maar het bestand dat FileResponse moet sturen niet. Let op: dit is een 500, geen 404 — de fout zit aan de serverkant, dus kijk in je terminal.

Oplossing: check of het bestand echt op die plek staat, met precies die naam (hoofdletters tellen). Het pad is relatief aan de map waar je fastapi dev startte, dus start de server vanuit je projectmap.

Meer uitleg: HTML in bestanden

Afbeelding laadt niet (broken image)

Oorzaak: het pad in src komt niet overeen met de plek van het bestand in je static-map.

Oplossing:

  1. Staat de afbeelding in de static folder?
  2. Klopt de bestandsnaam exact? (hoofdletters tellen)
  3. Klopt het pad in src="/static/foto.jpg"?
  4. Staat app.mount("/static", ...) in je code?

Meer uitleg: Afbeeldingen tonen

HTML wordt als tekst getoond (je ziet de tags)

Oorzaak: zonder response_class=HTMLResponse behandelt FastAPI je string als data, niet als HTML — de browser krijgt hem als platte tekst.

Oplossing:

# FOUT - toont HTML als tekst
@app.get("/pagina")
async def pagina():
return "<h1>Hallo</h1>"

# GOED - toont HTML als pagina
@app.get("/pagina", response_class=HTMLResponse)
async def pagina():
return "<h1>Hallo</h1>"

Meer uitleg: HTML tonen

Formulieren & POST

422 Unprocessable Entity

Oorzaak: FastAPI verwacht een veld dat niet binnenkomt. Bijna altijd: de name in je HTML matcht niet met de parameternaam in Python.

Oplossing:

  1. Heb je from fastapi import Form geïmporteerd?
  2. Staat name="..." op je <input> tags?
  3. Matcht de name in HTML met de parameter in Python?
  4. Staat method="post" op je <form> tag?
<input name="naam"> <!-- HTML -->
naam: str = Form(...) # Python: moet ook "naam" heten

Meer uitleg: Eigen POST request

Form data komt niet aan

Oorzaak: de browser verstuurt het formulier niet zoals je endpoint het verwacht — de method, het doel of een veldnaam wijkt af.

Oplossing:

  1. method="post" in de form-tag?
  2. action="/juiste_endpoint" in de form-tag?
  3. name="veldnaam" op elk input-veld?
  4. Parameternaam in Python gelijk aan de name in HTML?

Meer uitleg: Eigen POST request

Method Not Allowed (405)

Oorzaak: je stuurt een POST naar een GET-endpoint of andersom — het pad bestaat, maar niet voor deze soort verzoek.

Oplossing:

  • Formulier met method="post" → endpoint moet @app.post(...) zijn
  • Een URL in de adresbalk typen is altijd GET → endpoint moet @app.get(...) zijn

Krijg je de 405 direct ná het versturen van een formulier dat doorstuurt? Zie dan de aparte entry onder Templates.

Meer uitleg: GET vs POST

Templates (Jinja2)

TemplateNotFound error

Oorzaak: Jinja2 zoekt het bestand in de map die je bij Jinja2Templates(directory=...) opgaf, en vindt het daar niet.

Oplossing:

  1. Staat je template in de templates-map?
  2. Klopt de bestandsnaam in TemplateResponse(request, "bestand.html", ...) precies?
  3. Staat templates = Jinja2Templates(directory="templates") in je code?

Meer uitleg: POST met templates

Template variabele toont niets

Oorzaak: Jinja2 vult alleen in wat je meestuurt in het dictionary. Een naam die daar niet in zit wordt stilletjes leeg, zonder foutmelding.

Oplossing:

# FOUT - naam niet meegestuurd
return templates.TemplateResponse(request, "pagina.html", {})

# GOED - naam meegestuurd
return templates.TemplateResponse(request, "pagina.html", {"naam": naam})

Check ook dat de naam in {{ naam }} precies gelijk is aan de sleutel in het dictionary.

Meer uitleg: POST met templates

TypeError: unhashable type: 'dict'

Oorzaak: je gebruikt de oude volgorde, waarin request ín het dictionary staat. Die kom je nog overal online tegen, maar hij werkt niet meer — je bestandsnaam belandt op de plek van request en je dictionary op de plek van de bestandsnaam, dus FastAPI zoekt een template met een dictionary als naam.

Oplossing: zet request vooraan:

# FOUT - oude schrijfwijze
return templates.TemplateResponse("pagina.html", {"request": request, "naam": naam})

# GOED - request vooraan
return templates.TemplateResponse(request, "pagina.html", {"naam": naam})

Meer uitleg: POST met templates

TemplateSyntaxError: Unexpected end of template
jinja2.exceptions.TemplateSyntaxError: Unexpected end of template. Jinja was
looking for the following tags: 'endfor' or 'else'.

Oorzaak: een {% for %} of {% if %} in je template wordt nooit gesloten — het bestand is op terwijl het blok nog openstaat.

Oplossing: elke {% for %} heeft een {% endfor %} nodig, elke {% if %} een {% endif %}:

<!-- FOUT - de lus gaat nooit dicht -->
{% for bericht in berichten %}
<li>{{ bericht.naam }}</li>

<!-- GOED -->
{% for bericht in berichten %}
<li>{{ bericht.naam }}</li>
{% endfor %}

Meer uitleg: Een lijst tonen

Lijst blijft leeg terwijl er wel data in de database staat

Oorzaak: de lijst komt leeg of onder een andere naam bij de template aan — Jinja2 toont dan niets, zonder foutmelding.

Oplossing: check in deze volgorde:

  1. Staat de naam in {% for bericht in berichten %} precies gelijk aan de sleutel in {"berichten": ...}?
  2. Print alle_berichten in je endpoint. Zie je daar wél data, dan zit de fout in de template.
  3. Staat je database wel echt vol? Open /berichten nadat je een bericht hebt verstuurd, niet ervoor.

Meer uitleg: Een lijst tonen

AttributeError: 'NoneType' object has no attribute 'select'

Oorzaak: je gebruikt de data van je database buiten het with-blok, zonder er eerst een lijst van te maken. db.values() geeft geen lijst maar een generator: die haalt de rijen pas op als je erdoorheen loopt, en tegen die tijd is de database al dicht.

Oplossing:

# FOUT - de generator wordt pas buiten het with-blok gebruikt
with SqliteDict("gastenboek.db") as db:
alle_berichten = db.values()

# GOED - list() haalt de data binnen het with-blok op
with SqliteDict("gastenboek.db") as db:
alle_berichten = list(db.values())

Meer uitleg: Een lijst tonen

Na het versturen van een formulier krijg je 405 Method Not Allowed

Oorzaak: je redirect mist status_code=303. De standaard is 307, en die laat de browser je POST herhalen op de nieuwe URL — waar alleen een @app.get staat.

Oplossing:

# FOUT - stuurt 307, de browser herhaalt je POST op de nieuwe URL
return RedirectResponse(url="/berichten")

# GOED - stuurt 303, de browser doet een GET
return RedirectResponse(url="/berichten", status_code=303)

Meer uitleg: Terug naar de lijst

JavaScript

Cannot read properties of null (reading 'addEventListener')

Oorzaak: je script draait voordat het element bestaat. Zonder defer voert de browser je script uit terwijl hij nog in de <head> zit, en vindt querySelector niets.

Oplossing:

<!-- FOUT - draait terwijl de browser nog in de head zit -->
<script src="/static/js/app.js"></script>

<!-- GOED - wacht tot de pagina er staat -->
<script src="/static/js/app.js" defer></script>

Staat defer er wel? Check dan of de id in je HTML precies gelijk is aan die in je querySelector, inclusief hoofdletters.

Meer uitleg: JavaScript erbij

Mijn JavaScript-bestand wordt niet geladen (404 in de console)

Oorzaak: de browser kan het bestand niet vinden — verkeerde plek, verkeerd pad, of de static-map is niet gekoppeld.

Oplossing:

  1. Staat het bestand in static/js/?
  2. Staat app.mount("/static", StaticFiles(directory="static"), name="static") in je main.py?
  3. Begint het pad in je script-tag met een slash: src="/static/js/app.js"?

Open http://127.0.0.1:8000/static/js/app.js rechtstreeks in je browser. Zie je je code, dan ligt het aan de script-tag; krijg je een 404, dan aan het pad of de mount.

Meer uitleg: JavaScript erbij

Mijn controle in de HTML wordt genegeerd

Oorzaak: maxlength en required zijn instructies aan de browser, en de browser is van de bezoeker. Wie het formulier omzeilt, komt er gewoon langs.

Oplossing: wil je echt een grens, controleer dan óók in Python:

if len(bericht) > 80:
raise HTTPException(status_code=400, detail="Bericht is te lang")

Meer uitleg: Server of browser?

htmx

Na een klik staat de hele pagina in het doel (twee keer de kop)

Oorzaak: je endpoint geeft de hele pagina terug, of een omleiding. htmx volgt een omleiding net als de browser en zet alles wat binnenkomt in het doel van hx-target.

Oplossing: laat het endpoint alleen het stukje teruggeven dat in het doel hoort:

# FOUT
return RedirectResponse(url="/berichten", status_code=303)

# GOED
return HTMLResponse(f"Bedankt, {naam}. Je bericht staat in het gastenboek.")

Meer uitleg: Zonder herladen met htmx

Een knop doet niets, of het formulier verspringt met ?naam= in de adresbalk

Oorzaak: htmx is niet geladen. Zonder htmx zijn hx-get en hx-post attributen die de browser niet kent: een knop doet dan niets, en een formulier zonder method verstuurt hij als GET naar dezelfde pagina.

Oplossing: kijk in het tabblad Netwerk of htmx.min.js een 200 kreeg. Een 404 betekent dat het bestand niet op static/js/htmx.min.js staat, of dat de script-tag een ander pad noemt:

<script src="/static/js/htmx.min.js" defer></script>

Meer uitleg: Zonder herladen met htmx

Er gebeurt niets na een klik

Oorzaak: het verzoek is mislukt. htmx zet een foutantwoord (404, 500, of geen antwoord omdat de server uit staat) nooit in de pagina, dus op het scherm zie je niets.

Oplossing: open Netwerk en klik nog een keer. De rode regel zegt wat er mis is: 404 is een verkeerd pad in hx-get of hx-post, 500 een fout in je endpoint (kijk in de terminal), (mislukt) een server die niet draait.

Meer uitleg: Wat kan htmx allemaal

405 Method Not Allowed bij verwijderen

Oorzaak: de knop stuurt een DELETE (hx-delete), maar je endpoint staat op @app.post, of andersom.

Oplossing: werkwoord en decorator moeten overeenkomen:

# FOUT
@app.post("/bericht/{sleutel}")

# GOED
@app.delete("/bericht/{sleutel}")

Meer uitleg: Wat kan htmx allemaal

Database (sqlitedict)

ModuleNotFoundError: No module named 'sqlitedict'

Oorzaak: het pakket is niet geïnstalleerd in de omgeving waar je server in draait.

Oplossing: check dat je (.venv) in de terminal ziet en installeer:

python -m pip install sqlitedict

Meer uitleg: Gegevens opslaan

Data is weg na herstarten

Oorzaak: zonder db.commit() blijven je wijzigingen in het geheugen hangen en schrijft de database ze nooit naar het bestand.

Oplossing:

# FOUT - wijziging verdwijnt bij het sluiten
with SqliteDict("data.db") as db:
db["key"] = "waarde"

# GOED - commit schrijft naar het bestand
with SqliteDict("data.db") as db:
db["key"] = "waarde"
db.commit()

Meer uitleg: Gegevens opslaan

KeyError bij uitlezen

Oorzaak: je vraagt een sleutel op die niet in de database staat, en db[...] crasht daarop.

Oplossing: gebruik db.get() met een standaardwaarde:

# FOUT - crasht als de sleutel niet bestaat
waarde = db["naam"]

# GOED - geeft "Onbekend" als de sleutel niet bestaat
waarde = db.get("naam", "Onbekend")

Meer uitleg: Gegevens opslaan

Cookies & sessies

De cookie wordt niet onthouden

Oorzaak: set_cookie staat op een ander antwoord dan het antwoord dat je returnt.

Oplossing:

# FOUT - de cookie zit op een antwoord dat je weggooit
antwoord = RedirectResponse(url="/berichten", status_code=303)
antwoord.set_cookie(key="naam", value=naam)
return RedirectResponse(url="/berichten", status_code=303)

# GOED - dezelfde variabele erin en eruit
antwoord = RedirectResponse(url="/berichten", status_code=303)
antwoord.set_cookie(key="naam", value=naam)
return antwoord

Meer uitleg: Onthouden met een cookie

De cookie is er wel, maar mijn endpoint krijgt hem niet

Oorzaak: FastAPI zoekt de cookie op onder de naam van je parameter. Heet je parameter anders dan je cookie, dan vindt hij niets en krijg je de standaardwaarde.

Oplossing: heet je cookie sessie_id, dan heet je parameter ook sessie_id:

# FOUT - cookie heet 'sessie_id', parameter heet 'sid'
async def gastenboek_form(request: Request, sid: str = Cookie(default="")):

# GOED
async def gastenboek_form(request: Request, sessie_id: str = Cookie(default="")):

Meer uitleg: Onthouden met een cookie

De cookie verdwijnt zodra ik de browser sluit

Oorzaak: zonder max_age maak je een cookie die alleen bestaat zolang de browser openstaat.

Oplossing: geef een houdbaarheid in seconden mee, bijvoorbeeld dertig dagen:

antwoord.set_cookie(key="naam", value=naam, max_age=60 * 60 * 24 * 30)

Meer uitleg: Onthouden met een cookie

KeyError bij het uitlezen van een sessie

Oorzaak: je vraagt een sessie-id op dat niet in je database staat. Dat gebeurt bij een eerste bezoek, en zodra iemand zijn cookie aanpast.

Oplossing: gebruik .get() met een standaardwaarde in plaats van vierkante haken:

# FOUT - crasht bij een onbekend sessie-id
with SqliteDict("sessies.db") as sessies:
mijn = sessies[sessie_id]

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

Meer uitleg: Sessies

Iedereen krijgt dezelfde sessie te zien

Oorzaak: je gebruikt een vaste waarde als sessie-id in plaats van een willekeurige. Dan krijgt elke bezoeker dezelfde sleutel, en dus elkaars gegevens.

Oplossing: laat secrets het id maken:

# FOUT - iedereen deelt deze sessie
sessie_id = "sessie1"

# GOED - niet te raden, en voor elke bezoeker anders
sessie_id = secrets.token_hex(16)

Meer uitleg: Sessies

Mijn sessie wordt elke keer vergeten

Oorzaak: je maakt bij elk verzoek een nieuw sessie-id aan, ook als de bezoeker er al een had. De vorige sessie blijft dan onaangeroerd achter in je database.

Oplossing: maak alleen een nieuw id als er nog geen is:

# FOUT - overschrijft ook een bestaande sessie
sessie_id = secrets.token_hex(16)

# GOED
if not sessie_id:
sessie_id = secrets.token_hex(16)

Meer uitleg: Sessies

Algemeen

Het tabblad Netwerk is leeg, of mist het verzoek dat je zoekt

Oorzaak: Netwerk laat alleen zien wat het heeft opgenomen, en alleen wat door het filter komt. Dat verschilt per keer, en daarom is de lijst soms leeg en soms niet.

Oplossing: loop deze vier langs.

  1. Open het tabblad vóór je de pagina laadt, of herlaad met het tabblad open
  2. Zet Logboek behouden (Preserve log) aan als je een formulier verstuurt of naar een andere pagina gaat: anders wist elke nieuwe pagina de lijst
  3. Klik op Alle (All) in de rij soorten en maak het zoekvak leeg: een filter als Fetch/XHR blijft staan tot je hem uitzet
  4. Kijk of het rondje linksboven rood is; grijs betekent dat opnemen uit staat (Ctrl+E zet het aan en uit)

Meer uitleg: Kijken wat de browser doet

Wijzigingen zijn niet zichtbaar

Oorzaak: de browser toont zijn eigen bewaarde kopie van de pagina (de cache), of de server draait nog met je oude code.

Zelf vinden: druk op F12, kies het tabblad Netwerk en herlaad. Staat bij het bestand (schijfcache), dan heeft de browser de server niet eens gevraagd. Staat er 200 en is het bestand toch oud, dan draait de server met oude code.

Oplossing:

  1. Herlaad zonder cache: met de ontwikkelaarstools open, rechtermuisknop op de herlaadknop en Cache wissen en geforceerd opnieuw laden, of Ctrl+Shift+R
  2. Zet in het tabblad Netwerk het vinkje Cache uitzetten aan zolang je werkt
  3. Herstart de server (Ctrl+C, dan opnieuw fastapi dev main.py)
  4. Check of je het juiste bestand hebt aangepast

Meer uitleg: Kijken wat de browser doet

NameError: name 'app' is not defined

Oorzaak: je endpoint staat bóven de regel app = FastAPI(). Python leest je bestand van boven naar beneden, dus bij @app.get(...) bestaat app nog niet.

Oplossing: zet app = FastAPI() bovenaan, direct na de imports, en alle endpoints eronder:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
return {"bericht": "Hallo"}

Meer uitleg: Je eerste endpoint

ImportError / NameError bij andere namen

Oorzaak: je gebruikt iets dat niet geïmporteerd is — elke naam die je van FastAPI of sqlitedict gebruikt moet bovenaan je bestand staan.

Oplossing: de imports die je in deze cursus nodig hebt:

from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlitedict import SqliteDict
IndentationError

Oorzaak: Python leest de structuur van je code aan het inspringen af, en ergens klopt dat niet — vaak een mix van tabs en spaties, of een regel die te ver of te weinig inspringt.

Oplossing: gebruik overal vier spaties per niveau en mix geen tabs en spaties. In VS Code: selecteer alles en druk op Shift+Alt+F om automatisch te formatteren.