In this tutorial, you will get a complete breakdown of how to combine the power of FastAPI and Django to build a production-ready CRUD project. This project uses the best features from FastAPI, Pydantic for validation, and Django ORM and Admin.
Can you use FastAPI with Django?
You might be wondering if you can use FastAPI with Django, the short answer is yes. The long answer is yes of course. FastAPI and Django are excellent python frameworks as well as installable packages. Although they have their differences, we can borrow features from each to build a sophisticated project.
But, is FastAPI faster than Django?
Well, the benchmarks I have seen so far tend to put FastAPI at the top of the food chain concerning performance and speed. I have not done a speed test, from the project I have done so far, both have their advantages. I advocate for using the right tool for the right job.
This tutorial assumes you already know the basics of Python , Django , and FastAPI .
To demonstrate a CRUD project, we will be building a blog with FastAPI and Django combined. You will learn to:
Create base and specialized CRUD module for FastAPI.
Create a CRUD API Endpoint with FastAPI.
Set up validations using pydantic.
Set up Django ASGI and WSGI to work together with FastAPI.
Set up a basic API versioning with FastAPI.
Step 1: Install and Set Up Django and FastAPI
How to initialize a python virtual environment
python -m venv venv
source venv/bin/activate
Initialize a python virtual environment.
How to Install Django
pip install django
Install django.
How to Install FastAPI
pip install fastapi
pip install uvicorn
Install FastAPI and Uvicorn ASGI Server.
Step 2: Set up a Classic Django Project
mkdir src && cd src
django-admin startproject core .
Set up a Classic Django Project.
Step 3: Initialize a django blog app
How to create a blog app in Django
python manage.py startapp blog
create a blog app in Django.
Add the blog app to the installed app section in the Django settings file
INSTALLED_APPS = [
...,
"blog",
]
Add blog app to django installed app
How to create Django models for blog posts and category
Open the models.py
file and add the code below.
from datetime import date, datetime
from pathlib import Path
from typing import Any, Callable, List, Union
from django.conf import settings
from django.db import models
from pydantic import AnyUrl
from blog.managers import CategoryManager, PostManager
def upload_image_path(instance: Any, filename: str) -> Union[str, Callable, Path]:
"""
Set up the default thumbnail upload path.
"""
ext = filename.split('.')[-1]
filename = "%s.%s" %(instance.slug, ext)
return "blog_post_thumbnails/%s/%s" %(instance.slug, filename)
class Category(models.Model):
"""
Model for blog tags.
"""
title: str = models.CharField(max_length=200)
description: str = models.CharField(max_length=200, blank=True, null=True)
slug: str = models.SlugField(unique=True, blank=True)
parent: Union[str, int, list] = models.ForeignKey('self', null=True, default=1, related_name='tags', on_delete=models.SET_DEFAULT)
active: bool = models.BooleanField(default=False)
updated: datetime = models.DateTimeField(auto_now=True, auto_now_add=False)
created_on: datetime = models.DateTimeField(auto_now=False, auto_now_add=True)
objects: CategoryManager = CategoryManager()
class Meta:
constraints: List[Any] = [models.UniqueConstraint(fields=['title', 'slug'], name='unique_blog_category'),]
verbose_name_plural: str = "categories"
def __repr__(self) -> str:
return f"<class {self.slug}>"
def __str__(self) -> str:
return self.title
class Post(models.Model):
"""
Model for Blog Posts.
"""
user: Any = models.ForeignKey(settings.AUTH_USER_MODEL, default=1, null=True, on_delete=models.SET_DEFAULT, related_name="posts")
category: Any = models.ForeignKey(Category, null=True, default=1, blank=True, on_delete=models.SET_DEFAULT, related_name='post_category')
title: str = models.CharField(max_length=250)
description: str = models.TextField(null=True, blank=True)
content: str = models.TextField(default="Create a post.")
slug: str = models.SlugField(unique=True, blank=True)
thumbnail: Union[AnyUrl, str] = models.FileField(upload_to=upload_image_path, null=True, blank=True, max_length=1000)
draft: bool = models.BooleanField(default=False)
publish: date = models.DateField(auto_now=False, auto_now_add=False)
read_time: int = models.IntegerField(default=0)
view_count:int = models.PositiveIntegerField(default=0)
updated: datetime = models.DateTimeField(auto_now=True, auto_now_add=False)
created_on: datetime = models.DateTimeField(auto_now=False, auto_now_add=True)
objects: PostManager = PostManager()
class Meta:
verbose_name: str = "post"
verbose_name_plural: str = "posts"
ordering: list = ["-publish", "title"]
def __repr__(self) -> str:
return "<Post %r>" % self.title
def __str__(self) -> str:
return f"{self.title}"
def get_thumbnail_url(self) -> Union[Path, str, AnyUrl]:
timestamp = "%s%s%s%s%s" % (datetime.now().year, datetime.now().day, datetime.now().hour, datetime.now().minute, datetime.now().second)
if self.thumbnail and hasattr(self.thumbnail, 'url'):
return "%s?enc=%s" % (self.thumbnail.url, timestamp)
Create Django models for blog posts and category.
How to create model managers for blog posts and categories in Django
Create a managers.py
file in the blog folder and add the code below:
from datetime import timezone
from django.db import models
class PostQuerySet(models.query.QuerySet):
def active(self, *args, **kwargs):
return super(PostQuerySet, self).filter(draft=False).filter(publish__lte=timezone.now())
def search(self, query):
lookups = (models.Q(title__icontains=query) | models.Q(content__icontains=query) | models.Q(user__full_name__icontains=query))
return self.filter(lookups).distinct()
class CategoryManager(models.Manager):
def all(self):
return self.get_queryset()
def active(self, *args, **kwargs):
return super(CategoryManager, self).filter(active=True)
class PostManager(models.Manager):
def get_queryset(self):
return PostQuerySet(self.model, using=self._db)
def all(self):
return self.get_queryset()
def active(self, *args, **kwargs):
return super(PostManager, self).filter(draft=False).filter(publish__lte=timezone.now())
def full_search(self, query):
return self.get_queryset().search(query)
def filtered_search(self, query):
return self.get_queryset().active().search(query)
Create model managers for blog posts and categories.
How to create Django admin for blog posts and category models
In admin.py
, add the code below:
from django.contrib import admin
from blog.models import Category, Post
class BlogCategoryAdmin(admin.ModelAdmin):
list_display: list = ['title', 'active']
list_display_links: list = ['title']
list_filter: list = ['updated', 'active']
search_fields: list = ['title']
list_editable: list = ['active']
list_per_page: list = 10
ordering: tuple = ('-id',)
admin.site.register(Category, BlogCategoryAdmin)
class PostModelAdmin(admin.ModelAdmin):
list_display: list = ['title', 'updated', 'created_on', 'publish', 'draft']
list_display_links: list = ['title']
list_filter: list = ['updated', 'created_on', 'publish']
list_editable: list = ['draft']
search_fields: list = ['title', 'content']
list_per_page: int = 10
ordering: tuple = ('-id',)
class Meta:
model: Post = Post
admin.site.register(Post, PostModelAdmin)
Create Django admin for blog posts and category models.
How to migrate the django blog model
python manage.py makemigrations
python manage.py migrate
migrate the django blog model.
Now we are done with the Django Bit for the blog.
Step 4: Set Up FastAPI for the Blog APP
How to create pydantic schema validation for blog posts and category
Create the Pydantic Schema validation
create a schema.py
file inside the blog folder and insert the code below.
from datetime import date, datetime
from typing import Any, Generic, List, Optional, Type, Union
from accounts.schemas import UserOut
from pydantic import BaseModel, validator
from blog.models import Category
def confirm_title(value: str) -> str:
"""
Validation to prevent empty title field.
Called by the helper function below;
"""
if not value:
raise ValueError("Please provide a title.")
return value
def confirm_slug(value: str) -> str:
"""
Validation to prevent empty slug field.
Called by the helper function below;
"""
if not value:
raise ValueError("Slug cannot be empty.")
return value
class CategoryBase(BaseModel):
"""
Base fields for blog post category.
"""
title: str
description: Optional[str] = None
_confirm_title = validator("title", allow_reuse=True)(confirm_title)
class CreateCategory(CategoryBase):
"""
Fields for creating a blog category.
"""
...
class UpdateCategory(CategoryBase):
"""
Fields for updating blog categories.
"""
active: bool
class CategoryOut(CategoryBase):
"""
Response for blog post category.
"""
slug: str
active: bool
class Config:
orm_mode = True
class CategoryListOut(BaseModel):
"""
Response for list all categories.
We made a custom since we need just these two fields.
"""
title: str
slug: str
class Config:
orm_mode = True
class PostBase(BaseModel):
"""Base fields for blog posts."""
user: UserOut
title: str
# Validation for title and slug
_check_title = validator("title", allow_reuse=True)(confirm_title)
class CreatePost(PostBase):
"""
Fields for creating a blog post.
"""
...
class UpdatePost(PostBase):
"""
Fields for updating blog posts.
"""
view_count: int
active: bool
class SinglePost(PostBase):
"""
Response for a blog post.
"""
id: int
slug: str
view_count: int
draft: bool = False
publish: date
description: Optional[str] = None
content: Optional[str] = ...
read_time: int
category: CategoryOut
class Config:
orm_mode = True
class AllPostList(PostBase):\
"""
Response for listing all blog posts.
Custom for just these few fields
"""
id: int
slug: str
draft: bool = False
category: CategoryListOut
class Config:
orm_mode = True
class PostByCategoryList(PostBase):
"""
Response for all blog posts listing.
Custom for just these few fields.
"""
id: int
slug: str
draft: bool = False
class Config:
orm_mode = True
Create pydantic schema validation.
Create a base crud file inside the django core module
Create a base_crud.py
file in the core folder and insert the code below. This module serves as the base for all crud operations.
from typing import Generic, List, Optional, Type, TypeVar
from django.db.models import Model
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
ModelType = TypeVar("ModelType", bound=Model)
CreateSchema = TypeVar("CreateSchema", bound=BaseModel)
UpdateSchema = TypeVar("UpdateSchema", bound=BaseModel)
SLUGTYPE = TypeVar("SLUGTYPE", "int", "str")
class BaseCRUD(Generic[ModelType, CreateSchema, UpdateSchema, SLUGTYPE]):
"""
Base class for all crud operations
Methods to Create, Read, Update, Delete (CRUD).
"""
def __init__(self, model: Type[ModelType]):
self.model = model
def get(self, slug: SLUGTYPE) -> Optional[ModelType]:
"""
Get a single item.
"""
return self.model.objects.get(slug=slug)
def get_multiple(self, limit:int = 100, offset:int = 0) -> List[ModelType]:
"""
get multiple items using a query limiting flag.
"""
return self.model.objects.all()[offset:offset+limit]
def create(self, obj_in: CreateSchema) -> ModelType:
"""
Create an item.
"""
if not isinstance(obj_in, list):
obj_in = jsonable_encoder(obj_in)
return self.model.objects.create(**obj_in)
def update(self, obj_in: UpdateSchema, slug: SLUGTYPE) -> ModelType:
"""
Update an item.
"""
if not isinstance(obj_in, list):
obj_in = jsonable_encoder(obj_in)
return self.model.objects.filter(slug=slug).update(**obj_in)
def delete(self, slug: SLUGTYPE) -> ModelType:
"""Delete an item."""
self.model.objects.filter(slug=slug).delete()
return {"detail": "Successfully deleted!"}
Create a base crud file.
How to create FastAPI CRUD functions for blog posts and category
create an api_crud.py
file in the blog folder and insert the code below.
from typing import Generic, List, Optional, Type, TypeVar
from unicodedata import category
from core.base_crud import SLUGTYPE, BaseCRUD
from core.utils import unique_slug_generator
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model, Prefetch, query
from fastapi import Depends, HTTPException
from fastapi.encoders import jsonable_encoder
from blog.models import Category, Post
from blog.schemas import CreateCategory, CreatePost, UpdateCategory, UpdatePost
class PostCRUD(BaseCRUD[Post, CreatePost, UpdatePost, SLUGTYPE]):
"""
CRUD Operation for blog posts
"""
def get(self, slug: SLUGTYPE) -> Optional[Post]:
"""
Get a single blog post.
"""
try:
query = Post.objects.select_related("user", "category").get(slug=slug)
return query
except ObjectDoesNotExist:
raise HTTPException(status_code=404, detail="This post does not exists.")
def get_multiple(self, limit:int = 100, offset: int = 0) -> List[Post]:
"""
Get multiple posts using a query limit and offset flag.
"""
query = Post.objects.select_related("user", "category").all()[offset:offset+limit]
if not query:
raise HTTPException(status_code=404, detail="There are no posts.")
return list(query)
def get_posts_by_category(self, slug: SLUGTYPE) -> List[Post]:
"""
Get all posts belonging to a particular category.
"""
query_category = Category.objects.filter(slug=slug)
if not query_category:
raise HTTPException(status_code=404, detail="This category does not exist.")
query = Post.objects.filter(category__slug=slug).select_related("user").all()
return list(query)
def create(self, obj_in: CreatePost) -> Post:
"""
Create a post.
"""
slug = unique_slug_generator(obj_in.title)
post = Post.objects.filter(slug=slug)
if not post:
slug = unique_slug_generator(obj_in.title, new_slug=True)
obj_in = jsonable_encoder(obj_in)
query = Post.objects.create(**obj_in)
return query
def update(self, obj_in: UpdatePost, slug: SLUGTYPE) -> Post:
"""
Update an item.
"""
self.get(slug=slug)
if not isinstance(obj_in, list):
obj_in = jsonable_encoder(obj_in)
return Post.objects.filter(slug=slug).update(**obj_in)
def delete(self, slug: SLUGTYPE) -> Post:
"""Delete an item."""
self.model.objects.filter(slug=slug).delete()
return {"detail": "Successfully deleted!"}
class CategoryCRUD(BaseCRUD[Category, CreateCategory, UpdateCategory, SLUGTYPE]):
"""
CRUD Operation for blog categories.
"""
def get(self, slug: SLUGTYPE) -> Optional[Category]:
"""
Get a single category.
"""
try:
query = Category.objects.get(slug=slug)
return query
except ObjectDoesNotExist:
raise HTTPException(status_code=404, detail="This post does not exists.")
def get_multiple(self, limit:int = 100, offset: int = 0) -> List[Category]:
"""
Get multiple categories using a query limiting flag.
"""
query = Category.objects.all()[offset:offset+limit]
if not query:
raise HTTPException(status_code=404, detail="There are no posts.")
return list(query)
def create(self, obj_in: CreateCategory) -> Category:
"""
Create a category.
"""
slug = unique_slug_generator(obj_in.title)
category = Category.objects.filter(slug=slug)
if category:
raise HTTPException(status_code=404, detail="Category exists already.")
obj_in = jsonable_encoder(obj_in)
query = Category.objects.create(**obj_in)
return query
def update(self, obj_in: UpdateCategory, slug: SLUGTYPE) -> Category:
"""
Update a category.
"""
if not isinstance(obj_in, list):
obj_in = jsonable_encoder(obj_in)
return self.model.objects.filter(slug=slug).update(**obj_in)
def delete(self, slug: SLUGTYPE) -> Post:
"""Delete a category."""
Post.objects.filter(slug=slug).delete()
return {"detail": "Successfully deleted!"}
post = PostCRUD(Post)
category = CategoryCRUD(Category)
Create FastAPI CRUD functions.
How to create FastAPI endpoints for blog posts and category
Create an endpoints.py
file inside the blog folder and insert the code below.
from typing import Any, List
from fastapi import APIRouter
from blog.api_crud import category, post
from blog.schemas import (AllPostList, CategoryOut, CreateCategory, CreatePost, PostByCategoryList, SinglePost, UpdateCategory, UpdatePost)
router = APIRouter()
@router.get("/posts/", response_model=List[AllPostList])
def get_multiple_posts(offset: int = 0, limit: int = 10) -> Any:
"""
Endpoint to get multiple posts based on offset and limit values.
"""
return post.get_multiple(offset=offset, limit=limit)
@router.post("/posts/", status_code=201, response_model=SinglePost)
def create_post(request: CreatePost) -> Any:
"""
Endpoint to create a single post.
"""
return post.create(obj_in=request)
@router.post("/tags/", status_code=201, response_model=CategoryOut)
def create_category(request: CreateCategory) -> Any:
"""
Endpoint to create a single category.
"""
return category.create(obj_in=request)
@router.get("/tags/", response_model=List[CategoryOut])
def get_multiple_categories(offset: int = 0, limit: int = 10) -> Any:
"""
Get multiple categories.
"""
query = category.get_multiple(limit=limit, offset=offset)
return list(query)
@router.get("/posts/{slug}/", response_model=SinglePost)
def get_post(slug: str) -> Any:
"""
Get a single blog post.
"""
return post.get(slug=slug)
@router.get("/tags/{slug}/", response_model=List[PostByCategoryList])
def get_posts_by_category(slug: str) -> Any:
"""
Get all posts belonging to a particular category
"""
query = post.get_posts_by_category(slug=slug)
return list(query)
@router.put("/posts/{slug}/", response_model=SinglePost)
def update_post(slug: str, request: UpdatePost) -> Any:
"""
Update a single blog post.
"""
return post.update(slug=slug, obj_in=request)
@router.put("/tags/{slug}/", response_model=CategoryOut)
def update_category(slug: str, request: UpdateCategory) -> Any:
"""
Update a single blog category.
"""
return category.update(slug=slug, obj_in=request)
@router.delete("/posts/{slug}/")
def delete_post(slug: str) -> Any:
"""
Delete a single blog post.
"""
return post.delete(slug=slug)
@router.delete("/tags/{slug}/", response_model=CategoryOut)
def delete_category(slug: str) -> Any:
"""
Delete a single blog category.
"""
return category.delete(slug=slug)
Create FastAPI endpoints.
How to create a base FastAPI Router
Create an api_router.py
file in the core folder and insert the code below.
from blog.endpoints import router as blog_router
from fastapi import APIRouter
router = APIRouter()
router.include_router(blog_router, prefix="/blog", tags=["Blog"])
Create a base FastAPI Router.
Step 5: Set Up Fast API and Django Path
Add the code below to the settings file in the core folder. These variables are needed to separate the path of FastAPI from Django.
API_V1_STR: str = "/api/fa/v1"
WSGI_APP_URL: str = "/web"
PROJECT_NAME = "Django-FastAPI-Combo"
Set Up Fast API and Django Path.
Step 6: Configure ASGI for FastAPI Server
This step is the crux of it all. We want to configure the Django ASGI to allow uvicorn, hypercorn, or any ASGI server you choose to serve both ASGI and non ASGI paths.
import os
from importlib.util import find_spec
from django.apps import apps
from django.conf import settings
from django.core.wsgi import get_wsgi_application
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.wsgi import WSGIMiddleware
# Export Django settings env variable
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
apps.populate(settings.INSTALLED_APPS)
# This endpoint imports should be placed below the settings env declaration
# Otherwise, django will throw a configure() settings error
from core.api_router import router as api_router
# Get the Django WSGI application we are working with
application = get_wsgi_application()
# This can be done without the function, but making it functional
# tidies the entire code and encourages modularity
def get_application() -> FastAPI:
# Main Fast API application
app = FastAPI(title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json", debug=settings.DEBUG)
# Set all CORS enabled origins
app.add_middleware(CORSMiddleware, allow_origins = [str(origin) for origin in settings.ALLOWED_HOSTS] or ["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],)
# Include all api endpoints
app.include_router(api_router, prefix=settings.API_V1_STR)
# Mounts an independent web URL for Django WSGI application
app.mount(f"{settings.WSGI_APP_URL}", WSGIMiddleware(application))
return app
app = get_application()
Configure ASGI for FastAPI Server.
That is it!
Run the code below to access the FastAPI endpoints and Django Admin Page.
uvicorn core.asgi:app --reload
Access the FastAPI endpoints and Django Admin Page.
You can also run the django server. However, you may have to set up the User Model and create a superuser to get it working. We will not be covering that in this tutorial.
Wrap Off
So far, we have explored how to combine Django and FastAPI by creating a base and specialized CRUD module for FastAPI. We also looked at creating a CRUD API Endpoint with FastAPI, setting up validations using pydantic, setting up Django ASGI and WSGI to work together with FastAPI as well as setting up a basic API versioning with FastAPI.
Later on, we will learn to set up a user model to access the Django admin.