FastAPI Request Processing
Introduction
When you build web applications with FastAPI, understanding how requests are processed is essential. Request processing is the mechanism by which FastAPI receives, parses, validates, and makes available the data sent from clients to your application.
In this tutorial, we'll explore how FastAPI handles incoming HTTP requests, how to access different types of request data, and how to customize request processing to suit your application's needs.
How FastAPI Processes Requests
When a client sends an HTTP request to your FastAPI application, a sequence of operations occurs:
- Route matching: FastAPI matches the request path to a defined path operation
- Dependency resolution: Any dependencies required by the path operation are resolved
- Parameter extraction: Data is extracted from the request (path, query, headers, body)
- Data validation: The extracted data is validated against the defined models
- Function execution: Your path operation function is called with the validated data
- Response generation: The return value is converted to an HTTP response
Let's dive into each aspect of request processing.
Accessing Request Data
FastAPI provides several ways to access data from incoming requests. Let's explore each method:
Path Parameters
Path parameters are parts of the URL path that are variable and are captured as function parameters.
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
In this example, if a request comes to /items/42
, FastAPI will:
- Extract
42
from the URL path - Convert it to an integer (as specified by the type hint)
- Pass it to the function as
item_id
- Return
{"item_id": 42}
Query Parameters
Query parameters are the key-value pairs that appear after the ?
in a URL.
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
For a request to /items/?skip=20&limit=30
, FastAPI will:
- Extract the query parameters
skip=20
andlimit=30
- Convert them to integers
- Call the function with
skip=20
andlimit=30
- Return
{"skip": 20, "limit": 30}
If the URL doesn't include these parameters, the default values (0 and 10) are used.
Request Body
To receive JSON request bodies, you can define a Pydantic model and use it as a parameter:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
item_dict = item.dict()
if item.tax:
price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
When a POST request is made to /items/
with a JSON body, FastAPI will:
- Read the request body as JSON
- Validate the data against the
Item
model - Create an instance of
Item
with the data - Pass that instance to your function
Example request body:
{
"name": "Laptop",
"price": 999.99,
"tax": 100.00
}
Example response:
{
"name": "Laptop",
"description": null,
"price": 999.99,
"tax": 100.0,
"price_with_tax": 1099.99
}
Form Data
To handle form data, you'll need to install python-multipart
and use Form
:
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
File Uploads
FastAPI makes it easy to handle file uploads:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes = File(...)):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
contents = await file.read()
return {"filename": file.filename, "size": len(contents)}
Headers
You can access request headers in a similar way:
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/items/")
async def read_items(user_agent: str = Header(None)):
return {"User-Agent": user_agent}
The Request Object
Sometimes, you may need to access the raw request object. FastAPI provides access to this through the Request
class:
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/info")
async def get_request_info(request: Request):
client_host = request.client.host
method = request.method
url = request.url
headers = request.headers
query_params = request.query_params
return {
"client_host": client_host,
"method": method,
"url": str(url),
"headers": dict(headers),
"query_params": dict(query_params)
}
This is particularly useful when you need information about the request that isn't easily accessible through FastAPI's parameter injection.
Request Validation
One of FastAPI's most powerful features is automatic request validation:
from fastapi import FastAPI, Query
from typing import Optional
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Optional[str] = Query(
None,
min_length=3,
max_length=50,
regex="^[a-zA-Z]+$"
)
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
This validates that the query parameter q
:
- Is optional (None by default)
- Must be at least 3 characters long
- Cannot be more than 50 characters long
- Must match the regular expression (only alphabet characters)
If validation fails, FastAPI automatically returns an HTTP 422 Unprocessable Entity response with details about the validation error.
Real-world Example: Building a Simple API with Request Processing
Let's put all of this together in a more comprehensive example - a simple API for managing blog posts:
from fastapi import FastAPI, Path, Query, Body, HTTPException
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
app = FastAPI()
# Models
class PostBase(BaseModel):
title: str = Field(..., min_length=1, max_length=100)
content: str = Field(..., min_length=10)
published: bool = True
class PostCreate(PostBase):
pass
class Post(PostBase):
id: int
created_at: datetime
class Config:
orm_mode = True
# Fake database
posts_db = [
Post(
id=1,
title="First Post",
content="This is my first post content",
published=True,
created_at=datetime.now()
),
Post(
id=2,
title="Second Post",
content="This is my second post content",
published=False,
created_at=datetime.now()
)
]
# Path operations
@app.get("/posts/", response_model=List[Post])
async def get_posts(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
published: Optional[bool] = None
):
if published is None:
return posts_db[skip:skip+limit]
filtered_posts = [post for post in posts_db if post.published == published]
return filtered_posts[skip:skip+limit]
@app.get("/posts/{post_id}", response_model=Post)
async def get_post(post_id: int = Path(..., gt=0)):
for post in posts_db:
if post.id == post_id:
return post
raise HTTPException(status_code=404, detail="Post not found")
@app.post("/posts/", response_model=Post, status_code=201)
async def create_post(post: PostCreate = Body(...)):
new_post = Post(
id=len(posts_db) + 1,
**post.dict(),
created_at=datetime.now()
)
posts_db.append(new_post)
return new_post
@app.put("/posts/{post_id}", response_model=Post)
async def update_post(
post_id: int = Path(..., gt=0),
post_update: PostBase = Body(...)
):
for i, post in enumerate(posts_db):
if post.id == post_id:
updated_post = Post(
id=post_id,
**post_update.dict(),
created_at=post.created_at
)
posts_db[i] = updated_post
return updated_post
raise HTTPException(status_code=404, detail="Post not found")
@app.delete("/posts/{post_id}", status_code=204)
async def delete_post(post_id: int = Path(..., gt=0)):
for i, post in enumerate(posts_db):
if post.id == post_id:
posts_db.pop(i)
return
raise HTTPException(status_code=404, detail="Post not found")
This example demonstrates:
- Path parameter validation with
Path
- Query parameter validation with
Query
- Request body validation with Pydantic models and
Body
- Different HTTP methods (GET, POST, PUT, DELETE)
- Status code customization
- Error handling with
HTTPException
Summary
FastAPI's request processing is a powerful system that combines route matching, dependency injection, and data validation to create robust API endpoints. The key points to remember are:
- Type hints drive the validation and conversion of request data
- Pydantic models provide a way to define complex data structures for requests
- Path, Query, Body, and other classes allow fine-tuning the validation rules
- FastAPI automates the tedious parts of request processing like validation
- The Request object gives you access to lower-level details when needed
Understanding request processing is fundamental to building effective FastAPI applications, as it allows you to confidently accept and process user input while ensuring data correctness.
Additional Resources
Exercises
- Create a FastAPI endpoint that accepts a complex JSON object with nested fields and validates it using Pydantic models.
- Build an endpoint that accepts both query parameters and a request body, with appropriate validations for each.
- Create an API that accepts file uploads and performs validation on the file type and size.
- Implement a search endpoint with multiple optional filters as query parameters.
- Build a complete CRUD API for a resource of your choice with proper validation at all endpoints.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)