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 ORM
  • routers: FastAPI routers
  • adapters: The adapters to retrieve Django ORMs
  • schemas: 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.