FastAPI Tutorial Series Part 2: Query Parameters and Request Bodies
In Part 1, we learned how to create basic endpoints and use path parameters. Now let's explore how to handle different types of data from users.
Query Parameters Explained
Query parameters are the values that come after the ? in a URL, like:
http://127.0.0.1:8000/items?skip=0&limit=10
In this URL, skip and limit are query parameters.
Creating Endpoints with Query Parameters
Add this to your main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10):
items = [f"Item {i}" for i in range(skip, skip + limit)]
return {"items": items, "skip": skip, "limit": limit}
Now visit:
http://127.0.0.1:8000/itemshttp://127.0.0.1:8000/items?skip=5&limit=3http://127.0.0.1:8000/items?limit=20
Notice how:
- Function parameters become query parameters
- Default values (
= 0and= 10) make parameters optional - FastAPI automatically converts strings from the URL to integers
Optional vs Required Query Parameters
from typing import Optional
@app.get("/search")
def search_items(q: str, category: Optional[str] = None, max_price: Optional[float] = None):
result = {"query": q}
if category:
result["category"] = category
if max_price:
result["max_price"] = max_price
return result
Try these URLs:
-
http://127.0.0.1:8000/search?q=laptop(required parameter) http://127.0.0.1:8000/search?q=laptop&category=electronicshttp://127.0.0.1:8000/search?q=laptop&max_price=1000.50
Without the q parameter, you'll get an error because it's required (no default value).
Understanding Request Bodies
Request bodies are used to send complex data to your API, typically with POST, PUT, or PATCH requests. This is where Pydantic models come in.
What is Pydantic?
Pydantic is a library for data validation. It comes with FastAPI and makes sure the data you receive is exactly what you expect.
Creating Your First Pydantic Model
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
@app.post("/items")
def create_item(item: Item):
item_dict = item.dict()
if item.tax:
price_with_tax = item.price + item.tax
item_dict["price_with_tax"] = price_with_tax
return item_dict
Let's understand this:
-
class Item(BaseModel): Creates a data model -
name: str: Required string field -
description: Optional[str] = None: Optional string field -
price: float: Required float field -
tax: Optional[float] = None: Optional float field
Testing POST Requests
You can't test POST requests in your browser easily. Use the automatic documentation at http://127.0.0.1:8000/docs:
- Click on the POST
/itemsendpoint - Click "Try it out"
- Enter this JSON:
{
"name": "Laptop",
"description": "A powerful laptop",
"price": 999.99,
"tax": 99.99
}
- Click "Execute"
You'll see the response with the calculated price_with_tax.
What Happens if You Send Wrong Data?
Try sending this in the docs:
{
"name": "Laptop",
"price": "not a number"
}
FastAPI will automatically return an error explaining that price should be a number. This is Pydantic validation in action!
Combining Path, Query, and Body Parameters
You can use all three types in one endpoint:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
@app.put("/items/{item_id}")
def update_item(
item_id: int,
item: Item,
q: Optional[str] = None
):
result = {"item_id": item_id, **item.dict()}
if q:
result["q"] = q
return result
This endpoint accepts:
-
item_idfrom the path -
itemfrom the request body -
qas an optional query parameter
Test it in the docs with:
- URL:
/items/42?q=test - Body:
{
"name": "Updated Laptop",
"price": 1299.99
}
Request Body with Multiple Models
You can accept multiple models in one request:
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
class User(BaseModel):
username: str
email: str
@app.post("/purchase")
def purchase_item(item: Item, user: User):
return {
"message": f"{user.username} is purchasing {item.name}",
"total": item.price
}
Send this body:
{
"item": {
"name": "Laptop",
"price": 999.99
},
"user": {
"username": "john_doe",
"email": "john@example.com"
}
}
Practical Example: A Simple TODO API
Let's build something useful:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional, List
app = FastAPI()
class Todo(BaseModel):
id: Optional[int] = None
title: str
description: Optional[str] = None
completed: bool = False
# In-memory storage
todos = []
@app.get("/todos", response_model=List[Todo])
def get_todos(completed: Optional[bool] = None):
if completed is None:
return todos
return [todo for todo in todos if todo.completed == completed]
@app.post("/todos", response_model=Todo)
def create_todo(todo: Todo):
todo.id = len(todos) + 1
todos.append(todo)
return todo
@app.get("/todos/{todo_id}", response_model=Todo)
def get_todo(todo_id: int):
for todo in todos:
if todo.id == todo_id:
return todo
return {"error": "Todo not found"}
Try creating and retrieving todos using the docs interface!
Key Takeaways
- Query parameters come from the URL after
? - Request bodies come from POST/PUT requests and use Pydantic models
- Pydantic automatically validates your data
- You can combine path, query, and body parameters in one endpoint
- FastAPI converts Python types to JSON automatically
What's Next?
In Part 3, we'll cover:
- Handling errors properly with status codes
- Advanced Pydantic validation
- Response models
- Dependencies and security basics
Try building a simple API for something you're interested in. Maybe a recipe manager or a movie list? Share what you build in the comments!
Top comments (0)