Skip to course content
Free FastAPI backend course

FastAPI for AI Backend Development

Unit 04.01: Query parameters, defaults and optionality

Query parameters need defaults and bounds, and the bound is the one people leave out.

Declared, defaulted, constrained

A listing endpoint with a defaulted status and a bounded limit.

The code calls it three ways, including one over the bound.

from fastapi import FastAPI, Query
from fastapi.testclient import TestClient

app = FastAPI()


@app.get("/invoices")
def list_invoices(status: str = "open",
                  limit: int = Query(default=50, ge=1, le=200)) -> dict:
    return {"status": status, "limit": limit}


client = TestClient(app)
for url in ["/invoices", "/invoices?status=paid", "/invoices?limit=500"]:
    r = client.get(url)
    print(f"{url:26} -> {r.status_code} {r.json() if r.status_code == 200 else ''}")

print("\nThe bounded limit is the important one: without `le=200` a caller can")
print("ask for a million rows and your database will try to provide them.")

The bounded limit is the important one. Without le=200 a caller can ask for a million rows, your database will try to provide them, and the endpoint becomes a denial-of-service vector that you built yourself.

The bound is also in the published schema, so a caller sees the maximum rather than discovering it by being rejected.

The mistake this prevents

The mistake is adding pagination without a maximum, because the default is sensible. The default protects the caller who does not specify; the maximum protects you from the one who does.

Takeaway

Give every query parameter a default and a bound. An unbounded limit is a denial-of-service vector in your own code.