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 .
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"
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()
That is it!
Run the code below to access the FastAPI endpoints and Django Admin Page.
uvicorn core.asgi:app --reload
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.