Anchor Positioning Is Disruptive
New layouts will be possible
The more I play with it, the more convinced I am that anchor positioning is going to unlock some surprising new layouts.
FastAPI path operations are the equivalent of Django views. In this article we explore the differences, advantages, and gotchas of using them from the perspective of a Django developer.
Check out our Winging It channel for a conversation on FastAPI.
Winging It episode 5: Streamline Your API Development with FastAPI
If you’ve heard about FastAPI, a modern and fast web framework for building APIs with Python, you might be wondering how it compares to Django, the most popular and mature web framework for Python. In this series, I will answer this question by comparing various aspects and features of Django and FastAPI, based on our recent experience converting an internal project from Django to FastAPI.
I discovered Django when I wanted to explore web frameworks outside the ASP.NET and Windows ecosystem. I was impressed by its “batteries included” approach that provides everything you need to build a web application, from the database layer to the user interface. I also appreciate its “don’t repeat yourself” philosophy that encourages developers to write less code and focus on the business logic. For over a decade, Django has been my go-to framework for building web apps that are secure, performant, and a pleasure to work with.
In recent years, I have experienced two big shifts in the way I develop web applications. First, I expect development tools to do more for me when it comes to authoring software. Modern IDEs and code editors have really spoiled me with convenient features like go-to-definition, auto-completion, and one-click refactoring. This also means I expect languages and frameworks themselves to encourage best practices and help me write better code. Static type checking, automatic code formatting, and dependency injection are some of the features that I have a hard time living without.
Because Django pre-dates Python’s type checking system and it (rightly) wants to remain as backwards compatible as possible, all efforts to leverage static type checking and deeper text editor integration have been bolted-on, experimental, and incomplete. The main player in this space seems to be django-stubs, which provides type hints for Django as a separate package. After using it for a while, my conclusion is that Django was not designed with types in mind, and efforts to add them are mostly futile. The time and effort of adding and maintaining type hints for a Django app is not worth the limited benefits.
The second shift has to do with the proliferation of single-page applications and the need for cohesion and consistency across the API and frontend layers. Cohesion means that the API should provide a clear and logical way to access and manipulate the data and services that the backend offers. Consistency means that the API should follow common standards and conventions for data types, formats, errors, validations, and documentation.
Developing APIs with Django means you’re probably using the excellent Django
REST Framework (DRF for short). This package is a shining example of how Django
gives you complete and robust functionality with very little code (shout out to
you, ViewSet
). However, it suffers from the same problems as Django itself: it
was not designed with types in mind or to share information about endpoints and
serializers with consumers of its APIs. We tried to bridge this gap with
drf-spectacular, which produces OpenAPI schemas from DRF views and
serializers. Its main limitation is that it relies on developers to manually
annotate their application with additional information, and there’s no guarantee
that your schema will be up-to-date with your code. For this reason I wouldn’t
consider it a definitive solution.
In the middle of all this, I kept hearing about FastAPI and how it was not only fast, but also leveraged Python’s type system to provide a better developer experience and automatic documentation and schemas for API consumers. After following its excellent tutorial, I asked the team to consider it for OddBooks, our collaborative writing tool. An exploratory branch was created and after reviewing the resulting code, we decided to go ahead and officially switch to FastAPI for this project.
In OddBooks we have a Version
model that encapsulates the idea of a snapshot
of a document at a given point in time. Here’s a simplified Django model:
class Version(models.Model):
document = models.ForeignKey(Document, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=255)
text = models.TextField()
And the corresponding DRF serializer and view set that only allows editing the document and text during creation, not updates:
class VersionSerializer(serializers.ModelSerializer):
class Meta:
model = Version
fields = ["id", "document", "created_at", "title", "text"]
read_only_fields = ["id", "document", "created_at", "text"]
class VersionCreateSerializer(VersionSerializer):
class Meta(VersionSerializer.Meta):
read_only_fields = ["id", "created_at"]
class VersionViewSet(viewsets.ModelViewSet):
queryset = Version.objects.all()
serializer_class = VersionSerializer
def get_serializer_class(self):
if self.action == "create":
return VersionCreateSerializer
return super().get_serializer_class()
Notice a few things:
fields
and read_only_fields
. There’s also no
way to know the types of the fields without looking at the model definition
directly.Here’s an equivalent version written as FastAPI path operations (the equivalent of Django views):
from pydantic import BaseModel
from fastapi import FastAPI
class VersionUpdate(BaseModel):
title: str
class VersionCreate(BaseModel):
document: int
title: str
text: str
class VersionRead(BaseModel):
id: int
document: int
created_at: datetime
text: str
app = FastAPI()
@app.get("/versions", response_model=list[VersionRead])
def list_versions():
return get_versions_from_db()
@app.post("/versions", response_model=VersionRead, status_code=201)
def create_version(version: VersionCreate):
return write_version_to_db(**version.dict())
@app.put("/versions/{version_id}", response_model=VersionRead)
def update_version(version_id: int, version: VersionUpdate):
version = get_version_from_db(id=version_id)
version.title = version.title
version.save()
return version
@app.get("/versions/{version_id}", response_model=VersionRead)
def get_version(version_id: int):
return get_version_from_db(id=version_id)
@app.delete("/versions/{version_id}", status_code=204)
def delete_version(version_id: int):
delete_version_from_db(id=version_id)
Note: I’m hiding the actual database read and write operations behind
get_versions_from_db
and similar functions. How you connect to your database
is a separate topic and I want to focus on writing and consuming API endpoints
here.
In contrast with the Django version, we get:
version.
and your editor will show you the available fields and their
types.def update_version(id: int, version: VersionUpdate):
will only accept a JSON
body with a title
field and an integer URL parameter.response_model
parameter. FastAPI will ensure that the response body is a JSON object with
the expected fields and types. The path operation itself can return anything
that can be converted to JSON, including Pydantic models, dictionaries, lists,
and primitives.You will notice that the FastAPI version is considerably more verbose than the Django version. This is where Django’s “batteries included” approach really shines. However, I would argue that the verbosity is worth it for the benefits listed above, and by also nudging developers to be explicit in the input and output types of each individual endpoint, instead of relying on the hooks provided by DRF to serialize and deserialize data in different ways. You might even say we have traded one set of “batteries” for another.
FastAPI itself doesn’t have concepts of models or serializers. Instead, it relies on Pydantic models to validate data. These models are not meant to be used as representations of database tables, but rather as representations of the data that is sent and received by the API, so they are closer to DRF serializers.
I spent a non-trivial amount of time trying to make FastAPI behave like Django by trying to minimize the amount of Pydantic models. If Django only needs one or two serializers for all CRUD operations, why can’t FastAPI do the same? I started going down the rabbit hole of adding custom methods and properties, using inheritance, and in general introducing a lot of complexity to get that DRY magic back. I eventually realized that I was fighting against the framework instead of embracing it, and that I was better off writing small, focused Pydantic models for each endpoint.
So, is FastAPI worth considering? I would say yes, especially if you’re developing an API that needs to be consumed by a frontend application. The benefits of static type checking, automatic documentation, and automatic schema generation are too good to pass up. If you’re developing a traditional, multi-page application then the benefits are less clear and you might be better off sticking with Django because while FastAPI offers Jinja2 support for templating and easily serves static files as well, it lacks a built-in ORM and admin interface.
New layouts will be possible
The more I play with it, the more convinced I am that anchor positioning is going to unlock some surprising new layouts.
Performance, scope, and fallbacks for the anchor positioning polyfill
Our sponsors are supporting the continued development of the CSS Anchor Positioning Polyfill. Here’s a summary of the latest updates.
Are we measuring what we meant to measure?
There’s been a lot of interest in the results of the annual State of CSS survey, but are we asking all the right questions?