Dynamic Routes & Data: Master Path and Query Parameters

Chapter Title: Dynamic Routes & Data: Master Path and Query Parameters (covering: Extracting Data with Path Parameters: Dynamic URLs, Type Hinting and Automatic Validation for Path Parameters, Query Parameters: Filtering and Optional Data)

What You’ll Learn

In this chapter, you’ll gain the skills to build more flexible and powerful APIs by learning how to:

  • Create dynamic URLs that can accept variable data, known as path parameters.
  • Extract and use data from these path parameters within your FastAPI application.
  • Leverage Python’s type hinting for automatic data validation and conversion of path parameters, making your API more robust.
  • Define and utilize query parameters to enable filtering, pagination, and provide optional data to your API endpoints.
  • Understand how to make query parameters optional and assign default values, enhancing API usability.

Core Concepts

FastAPI allows you to define routes that are not fixed but can adapt based on parts of the URL. This dynamic behavior is crucial for building real-world APIs, enabling you to retrieve specific resources or filter collections of data.

Path Parameters: Dynamic URLs

Imagine you have a list of items, and you want to get details for a specific item using its ID. Instead of creating a separate route for every single item (/items/1, /items/2, etc.), you can define a single dynamic route. This is where path parameters come in.

A path parameter is a segment of the URL path that acts as a variable. You define it in your route decorator using curly braces {}.

Example: If you define a path /items/{item_id}, then item_id is a path parameter. When a user accesses /items/5, FastAPI will capture 5 as the value for item_id.

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id):
    return {"item_id": item_id}

In this example, item_id in the URL path maps directly to the item_id argument in the read_item function.

Type Hinting and Automatic Validation for Path Parameters

FastAPI isn’t just about routing; it’s also about making your code robust and catching errors early. By adding Python type hints to your path parameters, FastAPI automatically gains powerful features:

  1. Data Type Conversion: If you specify item_id: int, FastAPI will automatically attempt to convert the string value from the URL (e.g., “5”) into an integer (5).
  2. Data Validation: If the URL segment cannot be converted to the specified type (e.g., someone tries to access /items/abc when item_id is expected to be an int), FastAPI will automatically return a 422 Unprocessable Entity HTTP error with clear details, without you writing any explicit validation code.

This significantly reduces boilerplate code and improves API reliability.

Example with Type Hinting:

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int): # Now item_id is expected to be an integer
    return {"item_id": item_id, "type": type(item_id)}

If you access http://localhost:8000/items/123, item_id will be 123 (an int). If you access http://localhost:8000/items/abc, you’ll get a 422 error.

Query Parameters: Filtering and Optional Data

While path parameters are for identifying specific resources, query parameters are used for filtering, sorting, pagination, or providing optional information that doesn’t uniquely identify a resource. They appear after a question mark ? in the URL, with key-value pairs separated by ampersands &.

Example: /items/?skip=0&limit=10&search=keyboard

In FastAPI, any function parameters that are not part of the path (i.e., not defined in the {} syntax in the decorator) are automatically treated as query parameters.

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

In this example, skip and limit are query parameters.

Making Query Parameters Optional and Providing Default Values:

  • Optional Parameters: You can make a query parameter optional by providing a default value of None or by using Union[str, None] (or Optional[str] from typing).
  • Default Values: If you provide a default value (e.g., limit: int = 10), that value will be used if the client doesn’t provide the parameter in the URL. If no default is provided and the parameter is not marked as optional, it becomes a required query parameter.

Example with Optional and Default Query Parameters:

from fastapi import FastAPI
from typing import Optional # Or use 'str | None' in Python 3.10+

app = FastAPI()

@app.get("/products/")
async def search_products(query: Optional[str] = None, price_min: float = 0.0, limit: int = 10):
    results = {"query": query, "price_min": price_min, "limit": limit}
    if query:
        results["message"] = f"Searching for products matching '{query}'"
    else:
        results["message"] = "No specific query provided"
    return results
  • query: Optional[str] = None: query is an optional string parameter. If not provided, its value will be None.
  • price_min: float = 0.0: price_min is an optional float parameter with a default value of 0.0.
  • limit: int = 10: limit is an optional integer parameter with a default value of 10.

Hands-On Practice

Let’s put these concepts into practice. Remember to save your code (e.g., main.py) and run your FastAPI application using uvicorn main:app --reload.

Exercise 1: Simple Path Parameter

Create a FastAPI application with an endpoint that takes a user ID as a path parameter and returns a greeting message.

  1. Create a file named main.py.
  2. Add the following code:
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.get("/users/{user_id}")
    async def get_user_profile(user_id):
        return {"message": f"Welcome, User ID: {user_id}!"}
    
  3. Run your application: uvicorn main:app --reload
  4. Test in your browser or with a tool like Postman/curl:
    • Go to http://localhost:8000/users/123
    • Go to http://localhost:8000/users/john_doe

Expected Outcome:

  • For http://localhost:8000/users/123: {"message": "Welcome, User ID: 123!"}
  • For http://localhost:8000/users/john_doe: {"message": "Welcome, User ID: john_doe!"}

Exercise 2: Path Parameter with Type Hinting

Modify the previous exercise to ensure that the user_id is always treated as an integer. Observe what happens if you provide a non-integer value.

  1. Modify main.py:
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.get("/users/{user_id}")
    async def get_user_profile(user_id: int): # Added int type hint
        return {"message": f"Welcome, User ID: {user_id}! Type: {type(user_id).__name__}"}
    
  2. Restart your application (uvicorn should auto-reload, but double-check).
  3. Test:
    • Go to http://localhost:8000/users/456
    • Go to http://localhost:8000/users/abc

Expected Outcome:

  • For http://localhost:8000/users/456: {"message": "Welcome, User ID: 456! Type: int"}. Notice user_id is now an actual integer.
  • For http://localhost:8000/users/abc: You should receive a 422 Unprocessable Entity error with details about the validation failure. This is FastAPI’s automatic validation in action!

Exercise 3: Basic Query Parameters

Create an endpoint /search that accepts a required query string and an optional limit integer (defaulting to 10) as query parameters.

  1. Modify main.py:
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.get("/search/")
    async def search_items(query: str, limit: int = 10):
        return {"search_term": query, "result_limit": limit}
    
  2. Restart your application.
  3. Test:
    • http://localhost:8000/search/?query=fastapi
    • http://localhost:8000/search/?query=python&limit=5
    • http://localhost:8000/search/ (What happens here?)

Expected Outcome:

  • For http://localhost:8000/search/?query=fastapi: {"search_term": "fastapi", "result_limit": 10}
  • For http://localhost:8000/search/?query=python&limit=5: {"search_term": "python", "result_limit": 5}
  • For http://localhost:8000/search/: You should get a 422 Unprocessable Entity error because query is a required parameter and was not provided.

Exercise 4: Optional Query Parameters

Make the query parameter from Exercise 3 optional.

  1. Modify main.py:
    from fastapi import FastAPI
    from typing import Optional # Import Optional for older Python versions, or use 'str | None'
    
    app = FastAPI()
    
    @app.get("/search/")
    async def search_items(query: Optional[str] = None, limit: int = 10): # query is now optional
        results = {"result_limit": limit}
        if query:
            results["search_term"] = query
            results["message"] = f"Searching for '{query}'"
        else:
            results["message"] = "No search query provided."
        return results
    
  2. Restart your application.
  3. Test:
    • http://localhost:8000/search/
    • http://localhost:8000/search/?query=databases
    • http://localhost:8000/search/?limit=20

Expected Outcome:

  • For http://localhost:8000/search/: {"result_limit": 10, "message": "No search query provided."}
  • For http://localhost:8000/search/?query=databases: {"result_limit": 10, "search_term": "databases", "message": "Searching for 'databases'"}
  • For http://localhost:8000/search/?limit=20: {"result_limit": 20, "message": "No search query provided."}

Debugging Tip: When you get a 422 Unprocessable Entity error, FastAPI’s error message in the response body is usually very descriptive, telling you exactly which parameter failed validation and why. Always check the response details!

Real-World Application

Let’s apply these concepts to build a simple product catalog API.

Project 1: Product Catalog API

You’ll create a small API for a product catalog.

  1. Goal:

    • Retrieve details for a specific product using its ID (path parameter).
    • List products, allowing filtering by category and pagination using skip and limit (query parameters).
  2. Create main.py:

    from fastapi import FastAPI
    from typing import Optional
    
    app = FastAPI()
    
    # Simulate product data
    products_db = {
        1: {"name": "Laptop Pro", "category": "Electronics", "price": 1200.00},
        2: {"name": "Mechanical Keyboard", "category": "Electronics", "price": 150.00},
        3: {"name": "Notebook (A5)", "category": "Stationery", "price": 5.00},
        4: {"name": "Ergonomic Mouse", "category": "Electronics", "price": 75.00},
        5: {"name": "Gel Pen Set", "category": "Stationery", "price": 12.00},
        6: {"name": "Monitor UltraWide", "category": "Electronics", "price": 600.00},
    }
    
    @app.get("/products/{product_id}")
    async def get_product(product_id: int):
        """
        Retrieve details for a specific product by its ID.
        """
        if product_id not in products_db:
            return {"error": "Product not found"}, 404 # Manually returning a 404 for simplicity
        return products_db[product_id]
    
    @app.get("/products/")
    async def list_products(
        category: Optional[str] = None,
        skip: int = 0,
        limit: int = 10
    ):
        """
        List products, with optional filtering by category and pagination.
        """
        filtered_products = []
        for prod_id, product_info in products_db.items():
            if category is None or product_info["category"].lower() == category.lower():
                filtered_products.append({"id": prod_id, **product_info})
    
        # Apply pagination
        paginated_products = filtered_products[skip : skip + limit]
    
        return {
            "total": len(filtered_products),
            "skip": skip,
            "limit": limit,
            "products": paginated_products
        }
    
  3. Run: uvicorn main:app --reload

  4. Test the API:

    • Get a specific product: http://localhost:8000/products/1
    • Get a non-existent product: http://localhost:8000/products/99
    • List all products: http://localhost:8000/products/
    • List products with category filter: http://localhost:8000/products/?category=electronics
    • List products with pagination: http://localhost:8000/products/?skip=1&limit=2
    • Combine filter and pagination: http://localhost:8000/products/?category=stationery&skip=0&limit=1

This project demonstrates how path parameters identify unique resources and query parameters provide flexible ways to interact with collections of resources, all while leveraging FastAPI’s type hinting for validation.

Key Takeaways

  • Path Parameters are integral for creating dynamic URLs that identify specific resources (e.g., /users/{user_id}).
  • Type Hinting (e.g., user_id: int) with path parameters is a powerful FastAPI feature that provides automatic data conversion and robust validation, leading to cleaner code and better error handling.
  • Query Parameters are used for filtering, pagination, sorting, and providing optional data, appearing after the ? in the URL (e.g., /items/?skip=0&limit=10).
  • FastAPI automatically treats function parameters not in the path as query parameters.
  • You can make query parameters optional by assigning None as a default or using Optional[] (or | None in Python 3.10+), and provide default values for any parameter.

Before moving to the next chapter, ensure you are comfortable creating endpoints that use both path and query parameters, and understand how type hints influence their behavior. Experiment with invalid inputs to see FastAPI’s validation in action.

Quick Reference

FeatureSyntax ExampleDescription
Path Parameter@app.get("/items/{item_id}")Dynamic part of the URL, identifies specific resource.
Path Parameter (Function Arg)async def read_item(item_id: int):Function argument name must match path parameter name.
Type Hintingitem_id: intFastAPI automatically converts and validates item_id to an integer.
Query Parameterasync def search_items(query: str):Any function argument not in the path is a query parameter. Defaults to required.
Query Parameter (Default)async def search_items(limit: int = 10):limit is optional; if not provided, defaults to 10.
Query Parameter (Optional)async def get_user(name: Optional[str] = None): or `name: strNone = None`
Common Typesstr, int, float, boolFrequently used Python types for parameter hinting.
Run FastAPIuvicorn main:app --reloadCommand to start your FastAPI application.