Have you ever stared at FastAPI code, wondering how it magically knows what to do with all those function parameters? You’re not alone! FastAPI’s parameter handling system is incredibly powerful, but the way it works can feel like decoding alien tech when you’re first starting out.
Grab a coffee ☕️, and let’s demystify FastAPI parameters once and for all.
Path Parameters: Values from the URL
Let’s start with the simplest case. Path parameters come directly from the URL:
@app.get("/users/{user_id}")
def read_user(user_id: int):
return {"user_id": user_id}
When someone visits /users/42, FastAPI extracts 42 from the URL and passes it to your function as user_id. The type annotation (int) tells FastAPI to convert the string from the URL to an integer automatically.
Query Parameters: Values from ?key=value
Query parameters come from the URL query string (after the ?):
@app.get("/search/")
def search(q: str, page: int = 1, limit: int = 10):
return {"query": q, "page": page, "limit": limit}
A request to /search/?q=fastapi&limit=5 gives you {"query": "fastapi", "page": 1, "limit": 5}. Parameters with default values (like page and limit) are optional.
Request Body: JSON Data
For POST, PUT, and other methods that send data, you’ll typically use Pydantic models to handle JSON:
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
is_offer: bool = False
@app.post("/items/")
def create_item(item: Item):
return {"item_name": item.name, "price_with_tax": item.price * 1.1}
When your API receives JSON like {"name": "Coffee Mug", "price": 12.99}, FastAPI validates it against your model and creates a proper Python object.
Form Data & File Uploads
For handling form submissions and file uploads:
from fastapi import File, Form, UploadFile
@app.post("/upload/")
async def upload_file(
name: str = Form(...),
file: UploadFile = File(...)
):
return {
"filename": file.filename,
"name": name
}
The Form(...) and File(...) tell FastAPI to look for these values in form data rather than JSON.
The Magic of Dependency Injection
This is where FastAPI really shines. With the Depends() function, you can inject database connections, authentication, and more:
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
def get_db():
db = SessionLocal()
try:
yield db # The yield is important!
finally:
db.close() # This runs after your endpoint function
@app.get("/items/{item_id}")
def read_item(item_id: int, db: Session = Depends(get_db)):
item = db.query(Item).filter(Item.id == item_id).first()
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return item
When FastAPI sees Depends(get_db), it:
- Calls the
get_db()function - Takes what’s yielded (the database session)
- Passes it to your endpoint function
- After your function completes, it resumes
get_db()to run the cleanup code
This pattern is perfect for resource management - connections, files, etc.
Authentication with Dependencies
A common use of dependency injection is authentication:
def get_current_user(token: str = Header(...)):
if token != "secret-token":
raise HTTPException(status_code=401, detail="Invalid token")
return {"username": "john_doe"}
@app.get("/users/me")
def read_user_me(current_user: dict = Depends(get_current_user)):
return current_user
Here, get_current_user checks the token and either:
- Returns a user object that’s passed to your function
- Raises an exception, preventing your function from running
Special FastAPI Parameters (No Depends Required)
FastAPI automatically recognizes certain types and injects them without needing Depends():
from fastapi import Request, Response, BackgroundTasks
@app.get("/special")
async def special_parameters(
request: Request, # Raw HTTP request
response: Response, # HTTP response you can modify
background_tasks: BackgroundTasks, # For after-response processing
):
response.set_cookie("visited", "true")
background_tasks.add_task(log_visit, request.client.host)
return {"message": "Check your cookies!"}
When FastAPI sees parameters typed as Request, Response, or BackgroundTasks, it automatically provides the appropriate objects. That’s why in your original example, background_tasks: BackgroundTasks didn’t need Depends() - it’s special!
Understanding BackgroundTasks
BackgroundTasks deserves special attention because it’s incredibly useful:
def process_item(item_id: int):
# This runs after the response is sent
print(f"Processing item {item_id}")
# Imagine database operations, sending emails, etc.
@app.post("/items/{item_id}/process")
async def process_item_endpoint(
item_id: int,
background_tasks: BackgroundTasks
):
background_tasks.add_task(process_item, item_id)
return {"message": "Processing started"}
The task runs after your response is sent, allowing for faster API responses while heavy work happens in the background.
Dependency Chains: Dependencies with Dependencies
Dependencies can have their own dependencies, forming a chain:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_repository(db: Session = Depends(get_db)):
return Repository(db)
@app.get("/items/")
def read_items(repo: Repository = Depends(get_repository)):
return repo.get_all_items()
FastAPI resolves this entire chain automatically:
- Call
get_db()to get a database session - Pass that to
get_repository()to get a repository - Pass that repository to your endpoint
Shared Dependencies with Dependency Caching
If you use the same dependency multiple times, FastAPI is smart enough to call it just once per request:
def get_user(user_id: int, db: Session = Depends(get_db)):
return db.query(User).filter(User.id == user_id).first()
@app.get("/items/{item_id}")
def read_item(
item_id: int,
user: User = Depends(get_user),
db: Session = Depends(get_db) # Same db session as in get_user!
):
item = db.query(Item).filter(Item.id == item_id).first()
return {"item": item, "owner": user}
Even though get_db is used twice (once directly and once in get_user), FastAPI calls it only once.
Testing with Dependency Overrides
One of the most powerful features of dependency injection is how easy it makes testing:
# In your test file
from fastapi.testclient import TestClient
from app.main import app, get_db
def get_test_db():
test_db = TestingSessionLocal()
try:
yield test_db
finally:
test_db.close()
app.dependency_overrides[get_db] = get_test_db
client = TestClient(app)
def test_read_item():
response = client.get("/items/1")
assert response.status_code == 200
By replacing get_db with get_test_db, all your endpoints use a test database instead.
Elegant Parameter Types with Annotated (FastAPI 0.95.0+)
For cleaner code, FastAPI now supports Annotated from the typing module:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_user_agent(user_agent: str = Header(None)):
return user_agent
@app.get("/ua")
async def read_user_agent(
user_agent: Annotated[str, Depends(get_user_agent)]
):
return {"user_agent": user_agent}
This separates type information from dependency information, making your code more readable.
Putting It All Together: A Realistic Example
Here’s a comprehensive example combining multiple parameter types:
import uuid
from fastapi import BackgroundTasks, Depends, FastAPI, File, UploadFile
from sqlalchemy.orm import Session
app = FastAPI()
@app.post("/{case_id}/documents/upload", status_code=202)
async def upload_documents(
case_id: uuid.UUID, # From the URL path
background_tasks: BackgroundTasks, # Injected by FastAPI (server-side)
file: UploadFile = File(...), # From the client (multipart/form-data)
db: Session = Depends(get_db), # Injected by FastAPI (server-side)
current_user: User = Depends(get_current_active_user), # Injected by FastAPI (server-side)
):
# Store file information in the database
document = Document(
case_id=case_id,
filename=file.filename,
uploaded_by=current_user.id
)
db.add(document)
db.commit()
# Process the file in the background
background_tasks.add_task(
process_document,
document_id=document.id,
file=file,
user_id=current_user.id
)
return {"message": "Document upload accepted for processing"}
This single endpoint elegantly handles:
- Path parameters (
case_id) - File uploads (
file) - Database connections (
db) - Authentication (
current_user) - Background processing (
background_tasks)
When to Use What
To summarize when to use each parameter type:
- Path parameters: For required values that form part of the URL
- Query parameters: For optional filters, sorting, pagination
- Request body: For complex data in POST/PUT requests
- Form data: For traditional HTML forms
- File uploads: For file handling
- Dependencies: For shared resources, authentication, and business logic
- Special types: For direct access to request/response objects and background tasks
Conclusion
FastAPI’s parameter system might seem complex at first, but once you understand it, you’ll appreciate how it simplifies API development. By automatically handling everything from URL extraction to dependency injection, FastAPI lets you focus on your business logic rather than boilerplate code.
Remember, the basic rule is:
- Path and query parameters come from the URL
- Request body comes from the request data
- Dependencies and special types are provided by FastAPI
With these concepts in mind, you’ll be building clean, maintainable APIs in no time. Happy coding! 🚀
