Level Up Coding
Jul 19, 2024
2
Lately I have been using FastAPI for all my API development. I am a huge proponent of Django, however find myself coming back to FastAPI often.
This is an attempt to compile my reasons for preferring FastAPI over Django for API development.
Input Validation
FastAPI provides several kind of validations out of the box.
Consider an API endpoint to fetch a user’s profile. It expects the user_id
as a query parameter. user_id
is a required field.
It’s straight forward to achieve with FastAPI.
@app.get("/profile")
def profile(user_id: int):
...
...
return {"first_name": first_name, "last_name": last_name}
An API request to /profile
without query parameter user_id
would lead to response status code 422. API would enforce that the client sends a user_id
.
With Django, explicit validation would have to be added in the view.
def profile(request):
if 'user_id' not in request.GET:
content = {'message': 'user_id is required'}
return HttpResponse(json.dumps(content), status=422, headers={'Content-Type': 'application/json'})
...
...
Thus, simple validations like enforcing a required field can be handled seamlessly by FastAPI.
It doesn’t require any explicit application code.
However, Django needs explicit application code in such scenarios.
With only one query parameter, it is still manageable with Django. It rapidly becomes unwieldy when dealing with multiple parameters.
Consider an API which returns a list of items. It deals with 3 query parameters:
- category: required
- page: optional
- num_items: optional
With Django the view would appear like:
def list_items(request):
if 'category' not in request.GET:
message = 'category required'
return HttpResponse(message, status=422)
page = int(request.GET.get('page', 1))
num_items = int(request.GET.get('num_items', 20))
...
...
FastAPI path operation function would appear like:
def list_items(category: str, page: int = 1, num_items: int: 20):
...
...
No explicit input validation needed. FastAPI would enforce that category
is required. It will also ensure that page
and num_items
are optional.
Things become more interesting in POST requests, when application has to deal with request body.
Consider an API which allows creating an item. Assume item has attributes name
, description
and price
. name
and price
are required while description
is optional.
With Django, you would have to deserialise the request body, perform the validation in the view and then perform subsequent steps.
Otherwise, you would have to use an API framework, like DRF. If using DRF, the serialiser would still need to be used in the view to perform validation.
With FastAPI, the function wouldn’t require any validation code. A Pydantic model with type annotations can take care of the validation.
class Item(BaseModel):
name: str
description: str | None = None
price: int
@app.post("/items/")
def create_item(item: Item):
...
...
return {"name": item.name, "description": item.description, "price": item.price}
No validation code is needed in create_item
.
Request Parsing
With type annotation, FastAPI performs type conversion. Application code doesn’t need to perform any kind of type conversion.
Notice our last example around pagination. Django view code expects page
and num_items
to be integer. As query parameters are string, hence the Django view had to perform explicit type conversion.
page = int(request.GET.get('page', 1))
num_items = int(request.GET.get('num_items', 20))
With FastAPI, there was no need to use int()
.
We added type annotation for the function arguments page
and num_items
.
And FastAPI automatically took care of converting the string query parameter to an integer.
Things become more interesting while dealing with POST request body. Considering our previous example around create_item
.
Let’s assume the request body is:
{
"name": "iPhone",
"description": "The prime focus is your privacy",
"price": 2000
}
FastAPI will automatically parse this request body and pass an Item
instance as argument to create_item
. No parsing code would have to be written.
With Django, we would have to add an additional library like DRF to perform this parsing. And the DRF serialiser invocation would need to be added to the Django view.
Response serialization
With Django, you must explicitly perform the serialisation and return a HttpResponse
or it’s subclass.
Assume we want to return JSON {"message": "Hello World"}
. We need to explicitly perform the serialisation and wrap it in a HttpResponse
.
def root(request):
response = {"message": "Hello World"}
content = json.dumps(response)
return HttpResponse(content, headers={'Content-Type': 'application/json'})
You can return any serialisable type from a FastAPI path function. FastAPI would perform the appropriate serialisation.
def root():
return {"message": "Hello World"}
FastAPI is smart enough to also deal with some non serialisable types like datetime, Pydantic models and SQLAlchemy ORM models.
In our previous example of create_item
, we only need to say return item
and FastAPI would wisely return a JSON representation containing all attributes of Item.
@app.post("/items/")
def create_item(item: Item):
...
...
return item
The response would have the following structure.
{
"name": "iPhone",
"description": "The prime focus is your privacy",
"price": 2000
}
Automatic API schema
FastAPI creates the data JSON schema based on the Pydantic models and type annotations. It also generates an API schema compliant with OpenAPI.
With Django, we will have to plug in external dependencies to achieve the same and would require significant pipelining stuff.
Django Good Parts
We mostly discussed areas where FastAPI shines over Django.
There are areas where Django is almost unbeatable! Some of these include:
– Django Admin
– ORM
– Template engine
Thus the choice of the framework should solely depend on the target use case.
When building APIs, probably opt for FastAPI over Django. However if the application requires an admin user interface or say rendering the HTML pages on the server, then Django might be more suited.