Building Web Applications with Django and FastAPI: Combining the Best of Both Worlds
In python ecosystem there are multiple frameworks available for making a robust backend. Django and FastAPI are the most popular ones. Django comes with many out-of-the-box features such as ORM, Middleware, Authentication, Admin Panel etc. FastAPI on the other hand comes with Async-ready support and is a super fast, lightweight framework to generate Rest APIs.
The aim of this article is to explore how to leverage power of both frameworks in production. We will first be setting up a simple Django App and then will move towards integrating FastAPI.
Note: This article assumes that you are familiar with both frameworks and SQLAlchemy and have used them in past projects.
Django Part
I will be assuming that you have base django project ready at your end, if not please keep it ready before going forward. Here is the tutorial for doing so Your First Steps With Django: Set Up a Django Project — Real Python.
Firstly I will start basic Book app
django-admin startapp book
Add book app to installed apps
Adding models Author and Book
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author,on_delete=models.CASCADE)
published_date = models.DateField()
Making migrations and migrating changes to the database.
python manage.py makemigrations && python manage.py migrate
You might be able to see both new tables added with prefixes as app_name
in your database.
Make sure these two tables are configured. Now we will register these models in the admin panel and add some of my favorite books and their authors.
Screenshot of book samples:
authors:
Finally, we are done with setting up Django. Moving forward with the FastAPI setup
FastAPI setup
Unlike Django, FastAPI’s setup requires a bit of work. To people who are unfamiliar with FastAPI, I would recommend getting a bit more comfortable with it first and continuing the article. Adding some references from official documents:
- First Steps — FastAPI (tiangolo.com).
- GitHub — tiangolo/full-stack-fastapi-postgresql: Full stack, modern web application generator. Using FastAPI, PostgreSQL as database, Docker, automatic HTTPS and more.
( I use this project generation for FastAPI).
Make sure you have the base setup ready for FastAPI and Sqlalchemy with Alembic.
Here’s how my project looks. I will be adding a link to the repo at the end.
Let’s start with the database (named as db) part. We will be creating base model and binding the database engine with a session.
These are the files usually require to start with db connection. You can take this from the repo I have added in bottom.
session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
This is the usual setup for initializing session config, we will be initiating Metadata obj of sqlalchemy. Metadata class contains information on all the tables present in the given connection.
from sqlalchemy import create_engine, MetaData
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
metadata = MetaData(bind=engine)
Now comes, the part you might be waiting for. Adding models! Have a look at the files I have created. Currently, both files are empty!
We will be importing metadata
from app.db.session
and pointing to existing tables that we have already created.
books.py
from app.db.session import metadata, engine
from sqlalchemy import Table
book_table = Table('<table_name>',metadata,autoload_with=engine)
replace <table_name> with the exact one you have in the database in our example I will be using book_book for the book table and book_author for the author table.
Now only part left to completely assign a model is to add a table in its config.
from app.db.session import metadata, engine
from app.db.base import Base
from sqlalchemy import Table
book_table = Table('book_book',metadata,autoload_with=engine)
class Book(Base):
__table__ = book_table
author.py
from app.db.session import metadata,engine
from app.db.base import Base
from sqlalchemy import Table
author_table = Table('book_author',metadata,autoload_with=engine)
class Author(Base):
__table__ = author_table
Done? No! One more important step is still left. I know this article got long but stay with me.
We need to tell alembic not to do any migration on these models, as Django is managing these entities. You can also do the reverse, but I think that is a more complex version.
alembic/env.py
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
These are 2 methods to run and make migrations in the case of alembic. You have to change this above code to the given below code.
def include_object(object, name, type_, reflected, compare_to):
if type_ == "table" and reflected and compare_to is None:
return False
else:
return True
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
include_object=include_object,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata,include_object=include_object,
)
with context.begin_transaction():
context.run_migrations()
include_object function as the name suggests compares the tables, sequences, and relationships to pick for migrations.
We are avoiding the models which are reflected i.e. that is taken from already initialized tables.
That’s it now you are ready to run your APIs.
uvicorn app.main:app --reload
Try apis on open api docs. You will get similar results.
We are done here, now you can make fast performant APIs with FastAPI and at the same time use the admin panel with Django.
Thanks for reading through.