How to use FastAPI with Django ORM and Admin

Learn how to use FastAPI with Django ORM and Admin

Picture of Nsikak Imoh, author of Macsika Blog
How to use FastAPI with Django ORM and Admin written on a plain white background
How to use FastAPI with Django ORM and Admin written on a plain white background

This post is part of the tutorial series titled Learn to Use Django with FastAPI Frameworks

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.

Next Tutorial — How to Create User Model Using FastAPI and DjangoGet the Complete Code of Django and FastAPI Combo Tutorials on Github.

Connect with me.

Need an engineer on your team to grease an idea, build a great product, grow a business or just sip tea and share a laugh?