
Python has become a powerhouse for web development, offering several frameworks to build robust web applications and APIs. Two frameworks that have gained significant attention in recent years are Django and FastAPI. Django, the veteran, has been around since 2005 and is known for its "batteries-included" approach. FastAPI, the newcomer released in 2018, has quickly gained popularity for its speed, simplicity, and modern features. In this article, we'll compare these two frameworks specifically for building RESTful APIs, examining their approaches, performance characteristics, and development experiences.
The Philosophy Behind Each Framework
Django was designed as a complete web framework that follows the "batteries-included" philosophy. It provides almost everything a developer might need out of the box: an ORM for database interactions, an admin interface, authentication systems, and more. For API development, Django REST Framework (DRF) extends Django to make building RESTful APIs more straightforward. Django's primary goal is developer productivity and rapid development through its robust feature set.
FastAPI, on the other hand, was built specifically for API development with a focus on performance, ease of use, and modernity. It leverages Python type hints for validation and automatic documentation generation. FastAPI is built on Starlette for the web parts and Pydantic for the data validation, making it inherently asynchronous and extremely fast. Its philosophy revolves around simplicity, speed, and providing an intuitive developer experience without sacrificing performance.
Setting Up a Basic API
Let's compare how to set up a basic RESTful API for a simple "Book" resource in both frameworks. This will help illustrate the differences in their approaches.
Django + Django REST Framework Setup
First, let's create a basic Django project with Django REST Framework:
# Install required packages
pip install django djangorestframework
# Create a new Django project
django-admin startproject bookstore
cd bookstore
# Create a new app
python manage.py startapp books
Next, we need to add our app and REST framework to INSTALLED_APPS in settings.py:
# bookstore/settings.py
INSTALLED_APPS = [
# Django apps
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
# Local apps
'books',
]
Now let's define our Book model:
# books/models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.CharField(max_length=255)
published_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
price = models.DecimalField(max_digits=6, decimal_places=2)
def __str__(self):
return self.title
Next, we'll create a serializer to convert our model instances to JSON (and vice versa):
# books/serializers.py
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['id', 'title', 'author', 'published_date', 'isbn', 'price']
Now, let's create our API views:
# books/views.py
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
Finally, we'll set up the URL routing:
# books/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register('books', BookViewSet)
urlpatterns = [
path('', include(router.urls)),
]
# bookstore/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('books.urls')),
]
After creating the database and running migrations, our Django REST Framework API is ready to go. It provides full CRUD operations out of the box.
FastAPI Setup
Now let's implement the same API using FastAPI:
# Install required packages
pip install fastapi uvicorn sqlalchemy pydantic
# Create the main.py file
Here's a complete FastAPI implementation for our book API:
# main.py
from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy import create_engine, Column, Integer, String, Date, Float
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pydantic import BaseModel, Field
from datetime import date
from typing import List, Optional
# Database setup
SQLALCHEMY_DATABASE_URL = "sqlite:///./books.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# SQLAlchemy model
class BookModel(Base):
__tablename__ = "books"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
author = Column(String, index=True)
published_date = Column(Date)
isbn = Column(String, unique=True, index=True)
price = Column(Float)
# Create database tables
Base.metadata.create_all(bind=engine)
# Pydantic models for request/response
class BookBase(BaseModel):
title: str
author: str
published_date: date
isbn: str
price: float
class BookCreate(BookBase):
pass
class Book(BookBase):
id: int
class Config:
orm_mode = True
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Initialize FastAPI
app = FastAPI(title="Book API")
# API endpoints
@app.post("/api/books/", response_model=Book, status_code=201)
def create_book(book: BookCreate, db: Session = Depends(get_db)):
db_book = BookModel(**book.dict())
db.add(db_book)
try:
db.commit()
db.refresh(db_book)
except Exception:
db.rollback()
raise HTTPException(status_code=400, detail="ISBN already exists")
return db_book
@app.get("/api/books/", response_model=List[Book])
def read_books(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
books = db.query(BookModel).offset(skip).limit(limit).all()
return books
@app.get("/api/books/{book_id}", response_model=Book)
def read_book(book_id: int, db: Session = Depends(get_db)):
book = db.query(BookModel).filter(BookModel.id == book_id).first()
if book is None:
raise HTTPException(status_code=404, detail="Book not found")
return book
@app.put("/api/books/{book_id}", response_model=Book)
def update_book(book_id: int, book: BookCreate, db: Session = Depends(get_db)):
db_book = db.query(BookModel).filter(BookModel.id == book_id).first()
if db_book is None:
raise HTTPException(status_code=404, detail="Book not found")
for key, value in book.dict().items():
setattr(db_book, key, value)
try:
db.commit()
db.refresh(db_book)
except Exception:
db.rollback()
raise HTTPException(status_code=400, detail="Update failed")
return db_book
@app.delete("/api/books/{book_id}", status_code=204)
def delete_book(book_id: int, db: Session = Depends(get_db)):
db_book = db.query(BookModel).filter(BookModel.id == book_id).first()
if db_book is None:
raise HTTPException(status_code=404, detail="Book not found")
db.delete(db_book)
db.commit()
return None
To run the FastAPI application:
# Run the server
uvicorn main:app --reload
Key Differences in Development Experience
The code examples above highlight some fundamental differences in how these frameworks approach API development. Let's explore these differences in more detail.
Project Structure and Organization
Django enforces a specific project structure with distinct apps, models, views, and URLs. This organization helps with maintainability in large projects and follows Django's "one right way to do things" philosophy. Django's project structure encourages modular development and separation of concerns.
FastAPI, being more minimalist, provides flexibility in how you structure your application. You can have a single file for small applications or organize it into modules for larger projects. This flexibility is beneficial for microservices or smaller APIs but may require more discipline for larger applications to maintain organization.
Data Validation and Serialization
Django REST Framework uses serializers to validate incoming data and convert between complex types (like Django models) and Python native datatypes. Serializers provide a powerful abstraction that handles both validation and transformation of data.
FastAPI leverages Pydantic for data validation and serialization. Pydantic models use Python type hints to provide validation, which leads to clearer code and better IDE support. The validation errors are also more detailed by default. Furthermore, these types are used to generate automatic API documentation.
Automatic Documentation
One of FastAPI's standout features is its automatic interactive API documentation. By using OpenAPI and JSON Schema standards, FastAPI generates two interactive documentation interfaces: Swagger UI and ReDoc, with no additional configuration required.
# FastAPI automatically provides documentation at:
# http://localhost:8000/docs (Swagger UI)
# http://localhost:8000/redoc (ReDoc)
Django REST Framework provides browsable API documentation, but it's more basic and requires additional packages like drf-yasg or django-rest-swagger to get OpenAPI documentation similar to FastAPI's.
Asynchronous Support
FastAPI was built from the ground up to support asynchronous programming, which can significantly improve performance for I/O-bound operations. Here's how you'd write an asynchronous endpoint in FastAPI:
@app.get("/api/books/async/")
async def read_books_async():
# Perform async operations
await some_async_operation()
return {"books": results}
Django added ASGI support in version 3.0, but its ORM doesn't support async operations natively yet. Django REST Framework still primarily uses synchronous code. Django 4.0 has improved async support, but the ecosystem is still catching up to make full use of it.
Performance Comparison
FastAPI is known for its high performance, largely due to its use of Starlette and the ASGI standard. In benchmark tests, FastAPI consistently outperforms Django REST Framework, especially under high load. This is particularly relevant for APIs that need to handle many concurrent requests.
Consider this sample benchmark code that simulates concurrent requests:
# benchmark.py
import asyncio
import time
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
async def benchmark(url, num_requests):
async with aiohttp.ClientSession() as session:
start_time = time.time()
tasks = [fetch(session, url) for _ in range(num_requests)]
await asyncio.gather(*tasks)
end_time = time.time()
total_time = end_time - start_time
requests_per_second = num_requests / total_time
print(f"Completed {num_requests} requests in {total_time:.2f} seconds")
print(f"Requests per second: {requests_per_second:.2f}")
# Usage:
# asyncio.run(benchmark("http://localhost:8000/api/books/", 1000))
In typical scenarios, FastAPI can handle 2-3 times more requests per second than Django REST Framework for equivalent endpoints, especially when using asynchronous code for I/O-bound operations.
When to Choose Each Framework
Django REST Framework excels in scenarios where you need a full-featured web framework with an admin interface, robust authentication, and comprehensive ORM capabilities. It's particularly suitable for:
Projects that need both a traditional web interface and an API Teams familiar with Django's patterns and conventions Applications requiring complex database relationships and queries Projects that benefit from Django's extensive ecosystem and third-party packages Development teams that value consistency and "the Django way" of doing things
FastAPI shines in situations that demand high performance, modern Python features, and focused API development. It's ideal for:
Building high-performance APIs with many concurrent connections Microservices architecture where each service is focused and lightweight Projects that benefit from automatic documentation and schema validation Teams that value type hints and modern Python features Applications that need asynchronous processing for I/O-bound operations Developers who prefer explicit code over "magic" conventions
Real-World Example: User Authentication
Let's compare how both frameworks handle a common real-world scenario: user authentication for APIs.
Django REST Framework Authentication
Django REST Framework provides several authentication schemes out of the box, including token authentication:
# settings.py
INSTALLED_APPS = [
# ...
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}
# urls.py
from django.urls import path
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
# ...
path('api-token-auth/', obtain_auth_token, name='api_token_auth'),
]
# views.py
from rest_framework.permissions import IsAuthenticated
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [IsAuthenticated]
This provides a `/api-token-auth/` endpoint where users can obtain a token by sending their username and password. The token can then be included in the headers of subsequent requests.
FastAPI Authentication
FastAPI doesn't include authentication out of the box, but it's straightforward to implement using its dependency injection system and OAuth2 tools:
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional
# Setup
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Functions for authentication
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user_from_database(username)
if user is None:
raise credentials_exception
return user
# Token endpoint
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
# Protected endpoint example
@app.get("/api/books/")
async def read_books(current_user = Depends(get_current_user)):
# Only authenticated users can access this endpoint
return {"books": get_books_for_user(current_user)}
While FastAPI requires more code for authentication compared to Django REST Framework, it provides greater flexibility and explicit control over the authentication process. Additionally, FastAPI's dependency injection system makes it easy to secure individual endpoints based on different criteria.
Conclusion
Both Django REST Framework and FastAPI are excellent choices for building RESTful APIs in Python, but they cater to different needs and preferences. Django REST Framework provides a comprehensive, batteries-included approach with strong conventions and a rich ecosystem. It's particularly well-suited for complex applications that leverage Django's powerful features.
FastAPI offers a more modern, performance-focused approach with excellent type hints, automatic documentation, and native async support. It's ideal for developers who value speed, both in terms of development velocity and runtime performance, especially for high-concurrency applications.
The choice between these frameworks ultimately depends on your specific requirements, team expertise, and project constraints. For teams already invested in the Django ecosystem, Django REST Framework provides a natural extension with minimal learning curve. For new projects prioritizing performance and modern Python features, FastAPI offers compelling advantages that make it worth considering.
As with many technology choices, there's no one-size-fits-all answer. The best approach is to evaluate your specific needs and choose the framework that aligns most closely with your project goals and development philosophy.