Skip to content

Path & Query Parameters

URLs carry data in two ways:

https://api.example.com/users/42?lang=en&active=true
                         └─┬─┘ └─────────┬─────────┘
                       path param     query params

Path parameters

Use {name} in the route. Python's type hint tells FastAPI how to parse it.

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id, "name": f"User #{user_id}"}

Run with uvicorn main:app --reload. Then:

  • http://127.0.0.1:8000/users/42{"id": 42, "name": "User #42"}
  • http://127.0.0.1:8000/users/abc → automatic 422 error (not an int!)

That's automatic validation — free.

How it executes

When you hit /users/42:

  1. FastAPI matches /users/{user_id} against the URL.
  2. It extracts "42" as a string from the URL.
  3. It sees the type hint user_id: int — converts "42"42.
  4. If conversion fails (/users/abc), it returns HTTP 422 automatically — your function isn't even called.
  5. Function runs, return dict → JSON response.

Multiple path params

@app.get("/users/{user_id}/posts/{post_id}")
def get_post(user_id: int, post_id: int):
    return {"user": user_id, "post": post_id}

/users/3/posts/77{"user": 3, "post": 77}

Path parameter validation — Path(...)

For finer rules (min, max, regex):

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(
    item_id: int = Path(..., ge=1, le=1000, description="Item ID, 1-1000")
):
    return {"item_id": item_id}
  • ... means "required" (Ellipsis convention).
  • ge=1, le=1000 — value must be 1 ≤ id ≤ 1000.
  • description=... shows up in the Swagger docs.

/items/500 → ok. /items/9999 → 422: ensure this value is less than or equal to 1000.

Query parameters

Anything you put as a function argument that's not in the URL path becomes a query parameter.

@app.get("/products")
def list_products(category: str = "all", limit: int = 10):
    return {
        "category": category,
        "limit": limit,
        "products": []   # imagine a DB query here
    }
  • /productscategory="all", limit=10 (defaults).
  • /products?category=bookscategory="books", limit=10.
  • /products?category=books&limit=25 → both set.

Order doesn't matter. ?limit=25&category=books works the same.

Optional query parameters

Use | None = None (Python 3.10+ union syntax) or Optional:

@app.get("/search")
def search(q: str | None = None, page: int = 1):
    if q is None:
        return {"error": "missing query"}
    return {"query": q, "page": page, "results": []}

/search{"error": "missing query"} /search?q=fastapi{"query": "fastapi", "page": 1, ...}

Required query parameters

A parameter with no default value becomes required:

@app.get("/orders/{user_id}")
def get_orders(user_id: int, status: str):   # status has no default → required
    return {"user": user_id, "status": status}

/orders/5 → 422: field required. /orders/5?status=paid → ok.

Query parameter validation — Query(...)

Same idea as Path() but for query params:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/search")
def search(
    q: str = Query(..., min_length=3, max_length=50,
                   description="search term", example="fastapi"),
    limit: int = Query(10, ge=1, le=100),
):
    return {"q": q, "limit": limit}

/search?q=hi → 422: string too short.

Boolean query parameters

@app.get("/items")
def list_items(in_stock: bool = True):
    return {"in_stock_only": in_stock}

Accepted values for True: true, True, 1, yes, on. For False: false, False, 0, no, off.

List query parameters

?tags=python&tags=ml&tags=ai:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items")
def list_items(tags: list[str] = Query(default=[])):
    return {"tags": tags}

/items?tags=python&tags=ml{"tags": ["python", "ml"]}

Combining path + query

@app.get("/users/{user_id}/orders")
def user_orders(user_id: int, status: str = "all", limit: int = 20):
    return {
        "user": user_id,
        "status": status,
        "limit": limit,
    }

/users/5/orders?status=paid&limit=50

A complete example

# main.py
from fastapi import FastAPI, Path, Query

app = FastAPI(title="Product API")

@app.get("/")
def root():
    return {"message": "Product API — try /products"}

@app.get("/products")
def list_products(
    category: str | None = Query(None, description="filter by category"),
    limit: int = Query(10, ge=1, le=100),
    sort: str = Query("name", regex="^(name|price|date)$"),
):
    return {
        "category": category,
        "limit": limit,
        "sort": sort,
        "products": ["TBD — wire up the database"]
    }

@app.get("/products/{product_id}")
def get_product(
    product_id: int = Path(..., ge=1, description="positive integer"),
):
    return {"product_id": product_id, "name": f"Product #{product_id}"}

Run it:

uvicorn main:app --reload

Then try: - http://127.0.0.1:8000/products - http://127.0.0.1:8000/products?category=books&limit=5&sort=price - http://127.0.0.1:8000/products/42 - http://127.0.0.1:8000/docs — interactive

Common pitfalls

  • Forgetting the type hintdef get_user(user_id): (no type) treats it as a string with no validation. Always add : int, : str, etc.
  • /users/{user_id} but function param is named differently — names must match exactly.
  • Path params with default values — illegal. Path params are always required.
  • Mixing path and query order — order doesn't matter for function args, but path must match the URL exactly.

What's next

Practice

What does this print?

Expected: 42

# FastAPI converts path param "{item_id}" to int automatically via type hint
def get_item(item_id: int):
    return item_id
print(get_item(42))

Use type hint int so /items/abc returns 422 (not crashes the handler)

Expected: True

# Without type hint, abc becomes a string and may crash deeper in your code
def get_item(item_id):       # bug: no type hint — no validation
    return item_id * 2       # would do string repetition on "abc"
print(get_item("abc") == "abcabc")

Quiz — Quick check

What you remember

Q1. What's the difference between path params and query params?

  • Path params are part of the URL path (/items/{id}); query params come after ? (/items?id=42)
  • No difference
  • Path params are GET, query params are POST
  • Path params are optional

Why: Different positions in the URL. Path = required, hierarchical. Query = optional, filters/options. FastAPI handles both automatically based on whether the name appears in the route string.

Q2. What happens when /items/{item_id} is hit with /items/abc and the type is int?

  • Returns the string "abc"
  • Returns 422 Unprocessable Entity with a clear validation error
  • Crashes the server
  • Returns 200 with empty data

Why: FastAPI validates path params against the type hint. Conversion failure produces a 422 with a JSON body explaining what went wrong. Clean error responses for free.

Q3. How do you make a query parameter optional?

  • Give it a default value: def f(q: str = None) or def f(q: str | None = None)
  • Add optional=True
  • Use Optional[str] only
  • Wrap in a list

Why: Default value = optional. Without a default, FastAPI requires the param and returns 422 if missing. Use None (with Union type) for "may be omitted".

Common doubts

How do I validate that a query param is a positive integer?

Use Query() with constraints: from fastapi import Query; def f(q: int = Query(gt=0)). Adds the constraint to validation AND to the OpenAPI schema. Returns 422 for negative or zero values.

Can I have multiple values for one query param?

Yes — def f(ids: list[int] = Query()). Then /items?ids=1&ids=2&ids=3 gives you [1, 2, 3]. Modern Python type syntax works (list[int]); older requires List[int] from typing.

Path params order — does it matter?

Yes — function arg names must match the names in the route string. Order in the function signature doesn't matter for path params (FastAPI matches by name) but does for the URL itself.

Request Body & Pydantic Models