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:
- 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). - Data Validation: If the URL segment cannot be converted to the specified type (e.g., someone tries to access
/items/abcwhenitem_idis expected to be anint), FastAPI will automatically return a422 Unprocessable EntityHTTP 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
Noneor by usingUnion[str, None](orOptional[str]fromtyping). - 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:queryis an optional string parameter. If not provided, its value will beNone.price_min: float = 0.0:price_minis an optional float parameter with a default value of0.0.limit: int = 10:limitis an optional integer parameter with a default value of10.
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.
- Create a file named
main.py. - 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}!"} - Run your application:
uvicorn main:app --reload - 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
- Go to
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.
- 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__}"} - Restart your application (uvicorn should auto-reload, but double-check).
- Test:
- Go to
http://localhost:8000/users/456 - Go to
http://localhost:8000/users/abc
- Go to
Expected Outcome:
- For
http://localhost:8000/users/456:{"message": "Welcome, User ID: 456! Type: int"}. Noticeuser_idis now an actual integer. - For
http://localhost:8000/users/abc: You should receive a422 Unprocessable Entityerror 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.
- 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} - Restart your application.
- Test:
http://localhost:8000/search/?query=fastapihttp://localhost:8000/search/?query=python&limit=5http://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 a422 Unprocessable Entityerror becausequeryis a required parameter and was not provided.
Exercise 4: Optional Query Parameters
Make the query parameter from Exercise 3 optional.
- 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 - Restart your application.
- Test:
http://localhost:8000/search/http://localhost:8000/search/?query=databaseshttp://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.
Goal:
- Retrieve details for a specific product using its ID (path parameter).
- List products, allowing filtering by category and pagination using
skipandlimit(query parameters).
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 }Run:
uvicorn main:app --reloadTest 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
- Get a specific product:
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
Noneas a default or usingOptional[](or| Nonein 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
| Feature | Syntax Example | Description |
|---|---|---|
| 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 Hinting | item_id: int | FastAPI automatically converts and validates item_id to an integer. |
| Query Parameter | async 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: str | None = None` |
| Common Types | str, int, float, bool | Frequently used Python types for parameter hinting. |
| Run FastAPI | uvicorn main:app --reload | Command to start your FastAPI application. |