-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into pwa-article
* main: (36 commits) Add link to smashing Address review comments update author, slides, add mastodon link affiliate link add tags winging it 3 post Update workshop description, and add featured workshop Don't enforce shorthand properties Add link Automated webmentions update Automated dependency upgrades Fix date in link Schedule for Monday More background on Mapped and mapped_column Fix links More review Comma Adjust date Swapped batteries, more links More review ...
- Loading branch information
Showing
28 changed files
with
1,525 additions
and
516 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,9 @@ styles: | |
facebook_id: 1820980378150914 | ||
social: | ||
email: birds | ||
twitter: oddbird | ||
mastodon: | ||
user: oddbird | ||
server: front-end.social | ||
github: oddbird | ||
nav: | ||
- url: /work/ | ||
|
@@ -26,5 +28,5 @@ nav: | |
me: | ||
- https://www.oddbird.net/ | ||
- mailto:[email protected] | ||
- https://twitter.com/oddbird | ||
- https://front-end.social/@OddBird | ||
- https://github.com/oddbird |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,9 +6,9 @@ | |
{%- set footer_title = "Let's chat about your web project" -%} | ||
{%- set footer_content -%} | ||
Fill out the form, | ||
[schedule a call](https://calendly.com/oddbird/discovery), | ||
send us an [email](mailto:[email protected]), | ||
or find us on [Twitter](https://twitter.com/oddbird) or [GitHub](https://github.com/oddbird) | ||
[schedule a call](https://calendly.com/oddbird/discovery), or | ||
send us an [email](mailto:[email protected]). | ||
{{ contact.social(site.social, 'OddBird', false, rel='me') | safe }} | ||
{%- endset -%} | ||
|
||
{%- if add_cta %} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
264 changes: 264 additions & 0 deletions
264
content/blog/2023/fastapi-path-operations-for-django-developers.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
--- | ||
title: FastAPI Path Operations for Django Developers | ||
author: ed | ||
date: 2023-10-19 | ||
tags: | ||
- Article | ||
- Python | ||
- Django | ||
- FastAPI | ||
image: | ||
src: blog/2023/fast.jpg | ||
summary: | | ||
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. | ||
--- | ||
|
||
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. | ||
|
||
[FastAPI]: https://fastapi.tiangolo.com/ | ||
|
||
1. FastAPI Path Operations for Django Developers (this article) | ||
2. [SQLAlchemy for Django Developers] | ||
3. Testing a FastAPI Application (coming soon) | ||
4. How To Use FastAPI Dependency Injection Everywhere (coming soon) | ||
|
||
[SQLAlchemy for Django Developers]: /2023/10/23/sqlalchemy-for-django-developers/ | ||
|
||
## Why is FastAPI Worth Considering? | ||
|
||
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. | ||
|
||
[django-stubs]: https://github.com/typeddjango/django-stubs | ||
|
||
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. | ||
|
||
[Django REST Framework]: https://www.django-rest-framework.org/ | ||
[drf-spectacular]: https://github.com/tfranzel/drf-spectacular | ||
[OpenAPI]: https://swagger.io/specification/ | ||
|
||
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. | ||
|
||
[tutorial]: https://fastapi.tiangolo.com/tutorial/ | ||
[OddBooks]: https://oddbooks.app | ||
|
||
## Django Views | ||
|
||
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: | ||
|
||
```python | ||
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: | ||
|
||
```python | ||
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: | ||
|
||
- We don't get auto-complete or static type checking for the serializer fields. | ||
We are on our own to fill out `fields` and `read_only_fields`. There's also no | ||
way to know the types of the fields without looking at the model definition | ||
directly. | ||
- We get no documentation or schemas for the API endpoints. We have to manually | ||
write them and keep them up-to-date with the code. | ||
|
||
## FastAPI Path Operations | ||
|
||
Here's an equivalent version written as FastAPI path operations (the equivalent | ||
of Django views): | ||
|
||
```python | ||
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.* | ||
|
||
[connect to your database]: /2023/10/23/sqlalchemy-for-django-developers/ | ||
|
||
In contrast with the Django version, we get: | ||
|
||
- Auto-complete and static type checking for the model fields thanks to | ||
[Pydantic]. Need to see what fields are available on a version instance? Just | ||
type `version.` and your editor will show you the available fields and their | ||
types. | ||
- [Automatic documentation] and [OpenAPI schema] for the API endpoints. This is | ||
cohesive and consistent enough to be used to autogenerate frontend type | ||
definitions and [API clients]. We are actually doing this in OddBooks and it | ||
has done away with a handful of unit / integration tests and consistently | ||
warns the frontend team when the API has changed. | ||
- Runtime validation of the request body and URL parameters by using type hints. | ||
FastAPI will ensure that something like `def update_version(id: int, version: | ||
VersionUpdate):` will only accept a JSON body with a `title` field and an | ||
integer URL parameter. | ||
- Automatic serialization of the response body by using the `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. | ||
|
||
[Pydantic]: https://docs.pydantic.dev/latest/ | ||
[Automatic documentation]: https://fastapi.tiangolo.com/tutorial/path-params/#documentation | ||
[OpenAPI schema]: https://fastapi.tiangolo.com/tutorial/first-steps/#check-the-openapijson | ||
[API clients]: https://fastapi.tiangolo.com/advanced/generate-clients/ | ||
|
||
## Advice for Django Developers | ||
|
||
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. | ||
|
||
## Conclusion | ||
|
||
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]. | ||
|
||
[templating]: https://fastapi.tiangolo.com/advanced/templates/ | ||
[static files]: https://fastapi.tiangolo.com/tutorial/static-files/ | ||
[built-in ORM]: https://docs.djangoproject.com/en/4.2/topics/db/queries/ | ||
[admin interface]: https://docs.djangoproject.com/en/4.2/ref/contrib/admin/ |
Oops, something went wrong.