The integration of FastAPI and Django ORM
This is the English translation of the Japanese original post at qiita, with some modifications.
Motivation⌗
Recently FastAPI is growing incredibly. It’s blazingly fast and painless to develop, with 5~10x performance enhancement over Django or Flask.
I really want to switch to FastAPI from Django, however, it’s not that easy to give up Django and its self-sufficient user system as well as the admin page totally. I know it sounds greedy, but in fact there is such convenience. This time I’ll show you how to integrate FastAPI and Django ORM simply and quickly.
There’s also a demerit undoubtedly. Django ORM is not asynchronous and this will hurt performance.
If you’d like to improve, you may consider using orm or gino to rewrite some logic.
By the way, Django ORM is also having a plan for supporting asyncio.
Directory structure⌗
Let’s talk about the directory structure first. You can just follow Django’s tutorial to create the scaffold.
django-admin startproject mysite
django-admin startapp poll
If you’re done, delete views.py
and models.py
and prepare the folders for FastAPI like below.
$ tree -L 3 -I '__pycache__|venv' -P '*.py'
.
├── manage.py
├── mysite
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── polls
├── __init__.py
├── adapters
│ └── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models
│ └── __init__.py
├── routers
│ └── __init__.py
├── schemas
│ └── __init__.py
└── tests.py
7 directories, 15 files
The usage of each directory:
models
: Django ORMrouters
: FastAPI routersadapters
: The adapters to retrieve Django ORMsschemas
: FastAPI Pydantic models
For a typical FastAPI application, there should be an ORM part and a Pydantic model/validator part. For how to convert ORMs to Pydantic models, normally we’d like to utilize the ORM mode.
Set up some data⌗
Let’s refer to the Django documentation and insert some data:
>>> from polls.models import Choice, Question
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
>>> q.save()
FastAPI integration⌗
For simplicity, the codes below are all in the __init__.py
file of each folder, and I’ll also ignore some import
statements.
schemas
⌗
from django.db import models
from pydantic import BaseModel as _BaseModel
class BaseModel(_BaseModel):
@classmethod
def from_orms(cls, instances: List[models.Model]):
return [cls.from_orm(inst) for inst in instances]
class FastQuestion(BaseModel):
question_text: str
pub_date: datetime
class Config:
orm_mode = True
class FastQuestions(BaseModel):
items: List[FastQuestion]
@classmethod
def from_qs(cls, qs):
return cls(items=FastQuestion.from_orms(qs))
class FastChoice(BaseModel):
question: FastQuestion
choice_text: str
class Config:
orm_mode = True
class FastChoices(BaseModel):
items: List[FastChoice]
@classmethod
def from_qs(cls, qs):
return cls(items=FastChoice.from_orms(qs))
adapters
⌗
ModelT = TypeVar("ModelT", bound=models.Model)
def retrieve_object(model_class: Type[ModelT], id: int) -> ModelT:
instance = model_class.objects.filter(pk=id).first()
if not instance:
raise HTTPException(status_code=404, detail="Object not found.")
return instance
def retrieve_question(
q_id: int = Path(..., description="get question from db")
) -> Question:
return retrieve_object(Question, q_id)
def retrieve_choice(c_id: int = Path(..., description="get choice from db")):
return retrieve_object(Choice, c_id)
def retrieve_questions():
return Question.objects.all()
def retrieve_choices():
return Choice.objects.all()
routers
⌗
q_router = APIRouter()
c_router = APIRouter()
@q_router.get("/")
def get_questions(
questions: List[Question] = Depends(adapters.retrieve_questions),
) -> FastQuestions:
return FastQuestions.from_qs(questions)
@q_router.get("/{q_id}")
def get_question(
question: Question = Depends(adapters.retrieve_question),
) -> FastQuestion:
return FastQuestion.from_orm(question)
@c_router.get("/")
def get_choices(
choices: List[Choice] = Depends(adapters.retrieve_choices),
) -> FastChoices:
return FastChoices.from_qs(choices)
@c_router.get("/{c_id}")
def get_choice(choice: Choice = Depends(adapters.retrieve_choice)) -> FastChoice:
return FastChoice.from_orm(choice)
asgi.py
⌗
Let’s also add a FastAPI app into mysite/asgi.py
.
from fastapi import FastAPI
from polls.routers import choices_router
from polls.routers import questions_router
fastapp = FastAPI()
fastapp.include_router(questions_router, tags=["questions"], prefix="/question")
fastapp.include_router(choices_router, tags=["choices"], prefix="/choice")
Run servers⌗
First is to generate static files for uvicorn
(you may still need whitenoise):
python manage.py collectstatic --noinput
Now you can start FastAPI server by uvicorn mysite.asgi:fastapp --reload
and start Django server by uvicorn mysite.asgi:application --port 8001 --reload
.
Then you’ll see your favorite FastAPI’s OpenAPI documentation at http://127.0.0.1:8000/docs/
and don’t forget to check Django admin at http://127.0.0.1:8001/admin/
.
Conclusion⌗
It’s easier than we thought to integrate FastAPI and Django ORM, if you tactically separate the adapters of connecting Django ORM and Pydantic models, you’ll get a clear and concise directory structure - easy to write and easy to maintain.
The whole project is also at my Github, feel free to play with it.