DEV Community

FastAPI vs Django: Which Is Better? A Hands-On Comparison

If you've spent any time in Python web dev communities, you've seen this debate flare up at least once a week: FastAPI or Django?

Split-screen illustration comparing FastAPI and Django Python web frameworks, with a lightning bolt icon representing FastAPI's speed and a shield icon representing Django's robustness, separated by a VS divider on a dark background

Instead of giving you another opinion piece, this tutorial gets you building the same small API in both frameworks, side by side. You'll see the actual code, run both projects locally, hit some real errors, and walk away with a genuinely informed opinion instead of a borrowed one.

Grab a coffee — we're going to build, break, and fix things.

What We're Building

A minimal Task Manager API with these endpoints:

  • GET /tasks — list all tasks
  • POST /tasks — create a task
  • GET /tasks/{id} — get one task
  • PUT /tasks/{id} — update a task
  • DELETE /tasks/{id} — delete a task

We'll build it twice — once in FastAPI, once in Django (using Django REST Framework) — so you can compare the developer experience directly rather than trusting a benchmark chart.

Prerequisites

  • Python 3.10+
  • Basic familiarity with REST APIs
  • A terminal and a code editor
  • pip and venv working on your machine

If you're newer to Python web development in general and want structured, guided practice beyond this article, a good next step for many developers is enrolling in a python full stack training in bangalore program — it helps to have a mentor when you hit framework-specific gotchas that Stack Overflow answers don't quite cover.


Part 1: Setting Up Both Projects

Let's create two isolated environments so you can run both side by side.

mkdir fastapi-vs-django && cd fastapi-vs-django

# FastAPI project
mkdir fastapi_app && cd fastapi_app
python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install fastapi uvicorn[standard] sqlalchemy pydantic
cd ..

# Django project
mkdir django_app && cd django_app
python -m venv venv
source venv/bin/activate
pip install django djangorestframework
cd ..
Enter fullscreen mode Exit fullscreen mode

Keep two terminal tabs open — one per project — so you can run commands in parallel as we go.


Part 2: Building the FastAPI Version

Step 1 — Project structure

fastapi_app/
├── main.py
├── models.py
├── schemas.py
├── database.py
Enter fullscreen mode Exit fullscreen mode

Step 2 — Database setup (database.py)

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./tasks.db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
Enter fullscreen mode Exit fullscreen mode

Step 3 — Model (models.py)

from sqlalchemy import Column, Integer, String, Boolean
from database import Base

class Task(Base):
    __tablename__ = "tasks"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    done = Column(Boolean, default=False)
Enter fullscreen mode Exit fullscreen mode

Step 4 — Schemas (schemas.py)

from pydantic import BaseModel

class TaskCreate(BaseModel):
    title: str
    done: bool = False

class TaskResponse(TaskCreate):
    id: int

    class Config:
        from_attributes = True
Enter fullscreen mode Exit fullscreen mode

Step 5 — The API itself (main.py)

from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy.orm import Session
import models, schemas
from database import engine, SessionLocal, Base

Base.metadata.create_all(bind=engine)
app = FastAPI(title="Task Manager API")

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/tasks", response_model=list[schemas.TaskResponse])
def list_tasks(db: Session = Depends(get_db)):
    return db.query(models.Task).all()

@app.post("/tasks", response_model=schemas.TaskResponse)
def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db)):
    db_task = models.Task(**task.dict())
    db.add(db_task)
    db.commit()
    db.refresh(db_task)
    return db_task

@app.get("/tasks/{task_id}", response_model=schemas.TaskResponse)
def get_task(task_id: int, db: Session = Depends(get_db)):
    task = db.query(models.Task).filter(models.Task.id == task_id).first()
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    return task

@app.put("/tasks/{task_id}", response_model=schemas.TaskResponse)
def update_task(task_id: int, updated: schemas.TaskCreate, db: Session = Depends(get_db)):
    task = db.query(models.Task).filter(models.Task.id == task_id).first()
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    task.title = updated.title
    task.done = updated.done
    db.commit()
    return task

@app.delete("/tasks/{task_id}")
def delete_task(task_id: int, db: Session = Depends(get_db)):
    task = db.query(models.Task).filter(models.Task.id == task_id).first()
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    db.delete(task)
    db.commit()
    return {"message": "Task deleted"}
Enter fullscreen mode Exit fullscreen mode

Step 6 — Run it

uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

Visit http://127.0.0.1:8000/docs — FastAPI auto-generates interactive Swagger docs. This is genuinely one of its best features; you get a working test client for free, no extra setup.

Total lines of code: ~70.


Part 3: Building the Django Version

Step 1 — Create the project and app

django-admin startproject taskproject .
python manage.py startapp tasks
Enter fullscreen mode Exit fullscreen mode

Add rest_framework and tasks to INSTALLED_APPS in taskproject/settings.py:

INSTALLED_APPS = [
    ...
    'rest_framework',
    'tasks',
]
Enter fullscreen mode Exit fullscreen mode

Step 2 — Model (tasks/models.py)

from django.db import models

class Task(models.Model):
    title = models.CharField(max_length=200)
    done = models.BooleanField(default=False)

    def __str__(self):
        return self.title
Enter fullscreen mode Exit fullscreen mode

Step 3 — Serializer (tasks/serializers.py)

from rest_framework import serializers
from .models import Task

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ['id', 'title', 'done']
Enter fullscreen mode Exit fullscreen mode

Step 4 — Views (tasks/views.py)

from rest_framework import viewsets
from .models import Task
from .serializers import TaskSerializer

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
Enter fullscreen mode Exit fullscreen mode

Step 5 — URLs

tasks/urls.py:

from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

router = DefaultRouter()
router.register('tasks', TaskViewSet)

urlpatterns = router.urls
Enter fullscreen mode Exit fullscreen mode

taskproject/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('tasks.urls')),
]
Enter fullscreen mode Exit fullscreen mode

Step 6 — Migrate and run

python manage.py makemigrations
python manage.py migrate
python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

Visit http://127.0.0.1:8000/tasks/ — DRF gives you a browsable API UI automatically.

Total lines of code: ~40, but with more implicit "magic" happening behind the scenes (routers, viewsets, migrations).


Side-by-Side: What Actually Differs

Aspect FastAPI Django + DRF
Setup speed Fast, minimal boilerplate Slower, more scaffolding
Async support Native, first-class Improving, but ORM async is newer/less mature
Admin panel None built-in Free, powerful admin UI
ORM Bring your own (SQLAlchemy, Tortoise) Built-in, batteries-included
Validation Pydantic, very explicit Serializers, more implicit
Auto docs Built-in (Swagger/ReDoc) Requires drf-spectacular or similar
Learning curve Gentler for APIs specifically Steeper, but covers full-stack needs
Best for Microservices, ML model serving, high-throughput APIs Content-heavy apps, admin-driven platforms, monoliths

Neither of these is "better" in the abstract — they solve different problems well.


Practical Exercises

Try these to cement what you've learned:

  1. Add pagination to the /tasks list endpoint in both versions. Compare how much code each requires.
  2. Add a priority field (low, medium, high) to the Task model and expose it through both APIs.
  3. Add basic authentication — API key header in FastAPI, TokenAuthentication in DRF.
  4. Write a test for the POST /tasks endpoint using pytest (FastAPI) and Django's TestCase (DRF).
  5. Deploy both to Render or Railway and compare cold-start times.

If you want a repo to fork instead of starting from scratch, here are two solid open-source references to study:

Cloning either and reading through the folder structure will teach you more about "how experienced teams organize this stuff" than any tutorial can.


Common Errors and How to Fix Them

FastAPI: RuntimeError: Form data requires "python-multipart" to be installed
You forgot a dependency for form parsing. Fix:

pip install python-multipart
Enter fullscreen mode Exit fullscreen mode

FastAPI: Pydantic validation error on from_attributes
If you're on Pydantic v1, use orm_mode = True instead of from_attributes = True in your Config class. Check your Pydantic version with pip show pydantic.

Django: django.db.utils.OperationalError: no such table
You forgot to run migrations. Fix:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Django: CSRF verification failed on POST requests
This usually hits people testing with Postman against Django's default views (not DRF). If you're using DRF's APIView or ViewSet, CSRF is handled differently for session vs token auth — check your authentication classes in settings.py.

Both: Address already in use
Something's already running on the port. Kill it or change ports:

uvicorn main:app --reload --port 8001
python manage.py runserver 8001
Enter fullscreen mode Exit fullscreen mode

Performance Tips

  • FastAPI: use async def route handlers when you're doing I/O-bound work (calling external APIs, async DB drivers). If your DB driver is sync (like base SQLAlchemy), stick with regular def — mixing sync DB calls inside async def routes can actually block your event loop and hurt performance.
  • Django: use select_related() and prefetch_related() aggressively to avoid the N+1 query problem — this is the single biggest performance killer in Django apps.
  • Both: add database indexes on any field you filter or sort by frequently. Neither framework saves you from a poorly indexed database.
  • Both: use connection pooling in production (pgbouncer for Postgres is a common choice).
  • FastAPI: run with uvicorn behind gunicorn using uvicorn.workers.UvicornWorker for multi-process production deployments.
  • Django: cache expensive querysets with Django's cache framework (cache.set / cache.get) rather than hitting the DB on every request.

Best Practices Worth Adopting Either Way

  • Keep business logic out of your view/route functions — push it into service functions or a separate module. Both codebases above technically violate this for brevity; don't copy that part into production code.
  • Use environment variables for secrets (python-decouple or pydantic-settings for FastAPI; django-environ for Django) — never hardcode DB URLs or keys.
  • Write tests from day one. It's much harder to retrofit tests onto either framework later.
  • Version your API (/v1/tasks) if you expect it to evolve — this is easy to forget until you have real users depending on your endpoints.
  • Use type hints everywhere in FastAPI — it's not optional, it's the whole point of the framework's design.

So, Which Should You Actually Pick?

  • Building a standalone API, a microservice, or something that talks to an ML model or another service? FastAPI will get you there faster with less ceremony.
  • Building something with an admin backend, user accounts, content management, or a lot of "batteries-included" needs? Django (with or without DRF) will save you weeks of reinventing wheels Django already has.
  • Building a monolith that also needs a clean JSON API? Django + DRF is a very reasonable single choice.
  • Want to learn both well? Honestly, do exactly what this tutorial did — build the same small project twice. It's the fastest way to internalize the trade-offs instead of memorizing a comparison table.

If you're working through this kind of comparison as part of a structured learning path, look for a course that has you build real projects in both frameworks rather than just watching lecture videos — that hands-on repetition is what actually makes the differences stick. Searching for the best python full stack training in bangalore programs, you'll want to specifically check whether their syllabus includes project-based work like what we did above, not just framework theory.


Learning Resources


What did you build? If you worked through the exercises above, drop a link to your repo in the comments — I'd genuinely like to see how others structured the auth or pagination additions. That kind of comparison is usually more useful than any benchmark chart.

Top comments (0)