DEV Community

Vikas S
Vikas S

Posted on

FastAPI Tutorial Series Part 2: Query Parameters and Request Bodies

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

Now visit:

  • http://127.0.0.1:8000/items
  • http://127.0.0.1:8000/items?skip=5&limit=3
  • http://127.0.0.1:8000/items?limit=20

Notice how:

  • Function parameters become query parameters
  • Default values (= 0 and = 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
Enter fullscreen mode Exit fullscreen mode

Try these URLs:

  • http://127.0.0.1:8000/search?q=laptop (required parameter)
  • http://127.0.0.1:8000/search?q=laptop&category=electronics
  • http://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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Click on the POST /items endpoint
  2. Click "Try it out"
  3. Enter this JSON:
{
  "name": "Laptop",
  "description": "A powerful laptop",
  "price": 999.99,
  "tax": 99.99
}
Enter fullscreen mode Exit fullscreen mode
  1. 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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This endpoint accepts:

  • item_id from the path
  • item from the request body
  • q as an optional query parameter

Test it in the docs with:

  • URL: /items/42?q=test
  • Body:
{
  "name": "Updated Laptop",
  "price": 1299.99
}
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

Send this body:

{
  "item": {
    "name": "Laptop",
    "price": 999.99
  },
  "user": {
    "username": "john_doe",
    "email": "john@example.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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)