FastAPI Path & Query Parameters

Learn to handle path and query parameters in FastAPI with this focused tutorial. Understand core concepts, hands-on examples, and common edge cases for building better APIs.

Focus: handling path and query parameters

Sponsored

Have you ever built an API endpoint that needs to look up a user by ID, filter a list by a search term, or paginate results — only to realize you're not sure whether that value belongs in the URL path or as a query string? Mixing them up leads to confusing URLs, broken clients, and hours of debugging. In this lesson, you'll master FastAPI's path parameters (the /users/42 part) and query parameters (the ?active=true&page=2 part), so your endpoints are clean, predictable, and a joy to consume.

The problem this lesson solves

When you start building real APIs, you quickly hit a wall: every endpoint seems to need dynamic input. A user profile endpoint needs an ID. A product list endpoint needs filters and pagination. A search endpoint needs a term. Where does each piece of data go? If you cram everything into the path, you get URLs like /users/42/orders/true/2023, which are brittle and unreadable. If you put everything in the query string, you lose the RESTful clarity of /users/42.

The deeper problem is that frameworks make this harder than it needs to be. In Flask or Django, you often juggle request.args and URL converters with manual type casting — and the docs rarely explain why a parameter belongs in the path versus the query. FastAPI flips that: it gives you a declarative syntax with automatic validation, but only if you understand the rules. Without that understanding, you'll write endpoints that work by accident and break when a client sends ?user_id=abc and your code tries to compare it to an integer.

By the end of this lesson, you'll never guess again. You'll know the exact rule — path parameters for identifying a specific resource, query parameters for filtering, sorting, or modifying a request — and you'll be able to implement that rule with confidence in FastAPI.

Core concept / mental model

Think of a URL as a mailing address. The path is the street and building number — it tells the server which resource you want (/users/42). The query string is like a note you attach saying “handle with care” or “deliver to the back door” — it tells the server how to handle that resource (?verbose=true). You can't send a package to “building 42, note: red door” without a street. Similarly, you can't query a resource without first identifying it.

In FastAPI, the distinction is simple:

  • Path parameters are declared inside {} in the route decorator. They are required and appear in the URL itself.
  • Query parameters are declared as function arguments not in the path. They are optional (unless you give them no default) and appear after ? as key=value pairs.

Here's the key mental shift: in FastAPI, you don't manually parse the URL. You declare what you expect, and FastAPI does the extraction, type conversion, and validation for you. If a client sends a string where an int is expected, FastAPI returns an HTTP 422 error automatically.

Remember this rule: If a value identifies a specific resource (user ID, product slug, order number), it belongs in the path. If it describes how to retrieve or transform that resource (filter, sort, pagination), it belongs in the query.

How it works step by step

1. Define the route with path parameters

In your FastAPI app, you create an endpoint by decorating a function with @app.get("/users/{user_id}"). The {user_id} is a placeholder. FastAPI will match any value in that position and pass it to your function as an argument.

2. Declare the parameter type

Python type hints make FastAPI magic happen. If you write user_id: int, FastAPI converts the string from the URL to an integer and validates it. If conversion fails, it returns a 422 Unprocessable Entity error with a detailed message.

3. Add query parameters as optional arguments

Now add extra function arguments that aren't in the path, like active: bool = True or page: int = 1. FastAPI treats them as query parameters automatically. The default value makes them optional. If a client omits them, the default is used.

4. Test with automatic docs

FastAPI gives you interactive Swagger docs at /docs and ReDoc at /redoc. You can try every combination of path and query parameters right in the browser — no need to write curl commands for initial testing.

5. Use the values in your logic

Inside your function, the path and query values are just Python variables. You can use them for database lookups, filtering, or any business logic. FastAPI makes no assumptions about what you do with them — it just delivers them correctly.

Pro tip: Always declare path parameters first in the function signature, then query parameters. It's not enforced, but it keeps your code readable and matches the URL order.

Hands-on walkthrough

Let's build a real example: a small API for managing books. We'll have an endpoint to fetch a book by ID (path parameter) and another to list books with filters (query parameters).

First, create a virtual environment and install FastAPI:

python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install fastapi "uvicorn[standard]"

Now create main.py:

from fastapi import FastAPI, HTTPException
from typing import Optional

app = FastAPI()

# Mock database
db = [
    {"id": 1, "title": "The Pragmatic Programmer", "author": "Hunt & Thomas", "published": 1999},
    {"id": 2, "title": "Clean Code", "author": "Robert C. Martin", "published": 2008},
    {"id": 3, "title": "The Phoenix Project", "author": "Gene Kim", "published": 2013},
]

@app.get("/books/{book_id}")
async def get_book(book_id: int):
    """Fetch a book by its ID — a path parameter."""
    for book in db:
        if book["id"] == book_id:
            return book
    raise HTTPException(status_code=404, detail="Book not found")

Run it with:

uvicorn main:app --reload

Visit http://127.0.0.1:8000/books/2 — you'll get {"id":2,"title":"Clean Code",...}. Try http://127.0.0.1:8000/books/abc and you'll see the automatic 422 validation error.

Now let's add query parameters for filtering and pagination:

@app.get("/books/")
async def list_books(
    author: Optional[str] = None,
    min_year: Optional[int] = None,
    sort_by: str = "id",
    limit: int = 10,
):
    """List books with optional filters and pagination."""
    result = db
    if author:
        result = [b for b in result if author.lower() in b["author"].lower()]
    if min_year:
        result = [b for b in result if b["published"] >= min_year]

    if sort_by == "title":
        result = sorted(result, key=lambda x: x["title"])
    elif sort_by == "published":
        result = sorted(result, key=lambda x: x["published"])

    return result[:limit]

Now test:

  • /books/?author=martin → returns the Clean Code book.
  • /books/?min_year=2000&sort_by=title → returns books published after 2000 sorted by title.
  • /books/?limit=2 → returns the first two books.

Combining path and query parameters

You can mix both in one endpoint. Here's an endpoint for reviews of a specific book, with pagination:

@app.get("/books/{book_id}/reviews")
async def get_reviews(book_id: int, page: int = 1, size: int = 10):
    """Get paginated reviews for a specific book."""
    # Simulating reviews
    all_reviews = [f"Review {i} for book {book_id}" for i in range(1, 31)]
    start = (page - 1) * size
    end = start + size
    return {
        "book_id": book_id,
        "page": page,
        "size": size,
        "total": len(all_reviews),
        "items": all_reviews[start:end],
    }

Call /books/1/reviews?page=2&size=5 and you'll get reviews 6–10. Notice how the path parameter book_id is required, while page and size are optional with sensible defaults.

Compare options / when to choose what

Situation Use path parameter Use query parameter
Fetch a specific user by ID /users/42 ❌ Avoid — ugly and non-RESTful
List users with a filter like active status ❌ Avoid — too many variations /users?active=true
Get a product by slug in SEO URLs /products/blue-widget ❌ Avoid
Pagination (page, size) ❌ Never /items?page=2&size=50
Search with a free-text term ❌ Avoid if dynamic /search?q=python
Nested resource like a user's orders /users/5/orders ❌ Avoid

Why not put everything in the path?

Imagine writing endpoints like /books/author/martin/year/2000/sort/title. That's a maintenance nightmare. Clients must remember the exact order, and adding a new filter breaks every existing URL. Query parameters are order-independent and optional, making your API far more flexible.

Why not put everything in the query?

If you use /books?book_id=1 instead of /books/1, you lose the intuitive RESTful hierarchy. Path parameters also allow better caching and are more readable in logs and browser history. When a resource is nested (like orders belong to a user), the path parameter captures that relationship naturally.

Troubleshooting & edge cases

1. "404 Not Found" when you expect a match

If your route is /books/{book_id} but you call /books/2/ (trailing slash), FastAPI might not match unless you've enabled redirects. By default, FastAPI (via Starlette) does redirect /books/2/ to /books/2 with a 307. But be consistent in your API design — pick one style and document it.

2. Type conversion error — 422 instead of 400

If a client sends /books/abc where you expect an int, FastAPI returns 422 Unprocessable Entity. Many beginners expect a 404 or 400. 422 is correct — it means the request is syntactically invalid. Don't try to catch this in your function; let FastAPI handle it. If you need a custom error message, you can override the exception handler, but for most apps the default is fine.

3. Query parameter with a reserved word

If your query parameter is named type or class, you can't use it directly as a Python variable. Use an alias:

from fastapi import Query

@app.get("/items/")
async def read_items(item_class: str = Query(..., alias="class")):
    return {"class": item_class}

Now a client calls /items/?class=book and item_class receives "book". This is a common gotcha when integrating with external systems.

4. Missing required query parameter

If you declare a query parameter without a default, it becomes required. For example:

@app.get("/search/")
async def search(q: str):  # required
    ...

If a client calls /search/ without ?q=..., they get a 422 error. But sometimes you want an optional parameter that can be None. Use Optional[str] = None explicitly.

5. Boolean query parameter parsing

?active=true works, but what about ?active=1 or ?active=yes? FastAPI follows Python's bool parsing: only true, 1, yes, on are accepted as True. Anything else (like 2 or maybe) causes a 422. Be explicit in your API docs so clients know the accepted values.

6. Order of parameter definitions

In Python, you can't have a non-default argument after a default one. So always put path parameters first, then required query parameters (if any), then optional ones with defaults. This is a Python language rule, not just a style preference.

What you learned & what's next

You now understand the core distinction: path parameters identify a specific resource, and query parameters filter, sort, or modify a request. You've seen how FastAPI uses Python type hints to automatically extract, convert, and validate both kinds, and you've built a small but complete API with a nested endpoint and pagination.

You also learned how to troubleshoot common errors like 422 validation failures and boolean parsing quirks, and you know when to choose path over query parameters to keep your API clean and RESTful.

Next up in the track: now that you can handle dynamic input via URL, the next lesson will dive into request bodies and Pydantic models — how to accept structured JSON data from clients, validate it, and turn it into Python objects. That's where FastAPI's real power shines.

Practice what you've learned by extending the books API: add a query parameter to filter by publication year using ge and le with Query, and write a function that returns an error if page is less than 1. Then visit /docs to see how your API documentation automatically reflects all your parameters.

Practice recap

Extend the books API from the lesson: add a new endpoint /books/ that accepts min_rating (float, default 0) and max_pages (int, optional) as query parameters. Use them to filter a list of books you create with dummy data. Then add validation with Query(ge=0, le=5) on min_rating and test in /docs — see what happens when you send a rating of 6 or a negative page count. This will cement your understanding of query parameter validation.

Common mistakes

  • Confusing path and query parameters: putting filter values in the path, leading to URLs like /books/author/martin/year/2000 that are brittle and hard to extend.
  • Expecting a 404 when a client sends a non-integer path value like /books/abc — FastAPI correctly returns 422 since the type conversion fails.
  • Using a reserved Python keyword as a query parameter name (e.g., class) without an alias, causing a syntax error.
  • Assuming ?active=1 will work — FastAPI only accepts true, 1, yes, on as boolean True, anything else triggers a 422.

Variations

  1. Use FastAPI's Path() and Query() objects to add extra validation like gt, le, or min_length directly on parameters.
  2. Use the alias argument in Query() to map Python-friendly parameter names to external ones (e.g., item_class for class).
  3. For older Python versions, use Optional[int] from typing instead of the int | None union syntax — both work in FastAPI.

Real-world use cases

  • User profile API: GET /users/{user_id} — the path parameter identifies the user, and optional query params like ?include=posts control response depth.
  • E-commerce catalog: GET /products?category=shoes&price_min=50&sort_by=price — query parameters drive filtering, sorting, and pagination for thousands of items.
  • Analytics dashboard endpoint: GET /reports/{report_id}/data?start=2024-01-01&end=2024-12-31&granularity=monthly — path chooses the report, query shapes the data range and aggregation.

Key takeaways

  • Path parameters identify a single resource (/users/{id}); query parameters modify a request (?active=true).
  • FastAPI automatically converts path/query values to your declared Python types and returns 422 if validation fails.
  • Declare path parameters in the route {} and query parameters as function arguments with defaults for optional behavior.
  • Use Optional[type] = None for optional query parameters and provide sensible defaults like page=1.
  • Reserved keywords and boolean parsing can trip you up — use alias in Query() and document accepted boolean values.
  • FastAPI's auto-generated Swagger docs reflect all your parameters, making testing and collaboration easier.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.