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 fully asynchronous and this will hurt performance sometimes.

If you’d like to improve, you may consider using orm, gino or sqlalchemy 1.4+ to rewrite some logic.

From Django 4.1, you can make asynchronous queries with Django ORM. Note that async transactions are not supported yet, and the PostgreSQL driver is still psycopg2.

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, I’ll 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

We can make async queries from Django 4.1.

ModelT = TypeVar("ModelT", bound=models.Model)


async def retrieve_object(model_class: Type[ModelT], id: int) -> ModelT:
    instance = await model_class.objects.filter(pk=id).afirst()
    if not instance:
        raise HTTPException(status_code=404, detail="Object not found.")
    return instance


async def retrieve_question(q_id: int = Path(..., description="get question from db")):
    return await retrieve_object(Question, q_id)


async def retrieve_choice(c_id: int = Path(..., description="get choice from db")):
    return await retrieve_object(Choice, c_id)


async def retrieve_questions():
    return [q async for q in Question.objects.all()]


async def retrieve_choices():
    return [c async for c in Choice.objects.all()]

routers

routers/__init__.py

from .choices import router as choices_router
from .questions import router as questions_router

__all__ = ("register_routers",)


def register_routers(app: FastAPI):
    app.include_router(questions_router)
    app.include_router(choices_router)

routers/choices.py

router = APIRouter(prefix="/choice", tags=["choices"])


@router.get("/", response_model=FastChoices)
def get_choices(
    choices: List[Choice] = Depends(adapters.retrieve_choices),
) -> FastChoices:
    return FastChoices.from_qs(choices)


@router.get("/{c_id}", response_model=FastChoice)
def get_choice(choice: Choice = Depends(adapters.retrieve_choice)) -> FastChoice:
    return FastChoice.from_orm(choice)

routers/questions.py

router = APIRouter(prefix="/question", tags=["questions"])


@router.get("/", response_model=FastQuestions)
def get_questions(
    questions: List[Question] = Depends(adapters.retrieve_questions),
) -> FastQuestions:
    return FastQuestions.from_qs(questions)


@router.get("/{q_id}", response_model=FastQuestion)
def get_question(
    question: Question = Depends(adapters.retrieve_question),
) -> FastQuestion:
    return FastQuestion.from_orm(question)

asgi.py

Let’s also add a FastAPI app into mysite/asgi.py.

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

fastapp = FastAPI()


def init(app: FastAPI):
    from polls.routers import register_routers

    register_routers(app)

    if settings.MOUNT_DJANGO_APP:
        app.mount("/django", application)  # type:ignore
        app.mount("/static", StaticFiles(directory="staticfiles"), name="static")


init(fastapp)

Run servers

First is to generate static files for uvicorn (you may still need whitenoise if you don’t mount the Django app with FastAPI):

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/.

If you just need one ASGI app, the Django app can be mounted on FastAPI app:

# in mysite/settings.py

MOUNT_DJANGO_APP = True

Then the Django admin page can be found at http://localhost:8000/django/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.