Hello! We've covered a lot of ground in Django, from the basics of MVT and models to more advanced features like security and caching. Now, we're going to bridge the gap between a traditional Django app and a modern, high-performance API.
This lesson covers some vital modern topics. We'll start with Django's new asynchronous capabilities. Then, we'll cover the essentials of testing, a non-negotiable skill for professional developers. Finally, we'll begin our deep dive into the most popular Django add-on: Django REST Framework (DRF), the tool you'll use to build powerful web APIs.
This is a modern Django feature. Traditionally, Django was purely synchronous. When a request came in, it was handled by one worker thread from start to finish. If that thread had to wait (e.g., for a slow database query or an external API call), it was blocked and couldn't do any other work.
Analogy: The Barista
Django now supports async views by running on an ASGI (Asynchronous Server Gateway Interface) server like Uvicorn. This is ideal for tasks that are I/O-bound (like making many external API calls at once).
import asyncio
from django.http import HttpResponse
# By defining the view with 'async def',
# Django knows to run it in an async context.
async def my_async_view(request):
# This allows other tasks to run
# while we are 'sleeping' (waiting for I/O).
await asyncio.sleep(5)
return HttpResponse("Done after 5 seconds!")
You still cannot write await Product.objects.all(), but not for the reason people usually give. A QuerySet is lazy - it is a query builder, not a coroutine - so there is nothing to await until you actually evaluate it.
Since Django 4.1 the ORM has a proper async query API: the same methods with an a prefix, plus async iteration. Django 5.0 extended this to related managers. So the old advice of “you must always wrap ORM calls in sync_to_async” is out of date.
from .models import Product
async def product_views():
# Async versions of the terminal methods (Django 4.1+)
product = await Product.objects.aget(pk=1)
count = await Product.objects.acount()
exists = await Product.objects.filter(active=True).aexists()
first = await Product.objects.order_by("name").afirst()
await Product.objects.acreate(name="Widget", price=9.99)
product.price = 12.50
await product.asave()
await product.adelete()
# Iterating a queryset asynchronously
async for p in Product.objects.filter(active=True):
print(p.name)
# Async comprehension to materialise a list
names = [p.name async for p in Product.objects.all()]
return names
The nuance that gets you the follow-up right: these async methods are an async-safe interface, not async database drivers. Django still executes the query on a thread under the hood, so you get correctness and clean syntax inside an event loop, but not the connection-level concurrency you would get from an natively async stack like SQLAlchemy's asyncio extension. Fully async database backends are still work in progress.
sync_to_async is still the right tool for ORM operations that have no a-prefixed variant, and for any other blocking work you need to call from async code:
from asgiref.sync import sync_to_async
# Still needed for sync-only code paths, e.g. a third-party
# library or a method without an async variant.
result = await sync_to_async(some_blocking_function)(arg)
Django has a powerful built-in testing framework that is an extension of Python's standard `unittest` module.
A unit test is a function that tests one small, isolated "unit" of your code (e.g., "Does my `Product` model correctly calculate its sale price?").
The key features of Django's testing are:
# my_app/tests.py
from django.test import TestCase
from django.urls import reverse
from .models import Product
class ProductTests(TestCase):
# This 'setUp' method runs *before* every test
def setUp(self):
# Create a sample product to test against
Product.objects.create(name="Test Product", price=100)
def test_product_model_str(self):
"""Test the model's __str__ method."""
product = Product.objects.get(name="Test Product")
self.assertEqual(str(product), "Test Product")
def test_product_list_view(self):
"""Test that the product list page loads."""
# 'reverse' finds the URL from its name
url = reverse('product-list')
# Use the test client to make a GET request
response = self.client.get(url)
# Check that the page loaded successfully
self.assertEqual(response.status_code, 200)
# Check that our product's name is in the HTML
self.assertContains(response, "Test Product")
# To run the tests for 'my_app':
# $ python manage.py test my_app
Sample Output:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.042s
OK
Destroying test database for alias 'default'...
Django REST Framework (DRF) is a powerful and flexible toolkit for building Web APIs in Django.
While standard Django is built to return HTML (web pages), DRF is built to return data (like `JSON` or `XML`).
Analogy: The Restaurant, Revisited
DRF provides all the "batteries-included" features Django is famous for, but for APIs:
A Serializer is the translator in DRF. Its job is to convert complex data types, like Django model instances, into native Python datatypes (like dictionaries) that can then be easily rendered into `JSON`.
It also does the reverse: deserialization. It takes incoming `JSON` data, validates it, and converts it back into a Python object or model instance.
The two main types are just like Django's forms:
# my_app/serializers.py
from rest_framework import serializers
from .models import Product
# This ModelSerializer will:
# 1. Automatically create 'name' and 'price' fields
# 2. Automatically validate them (e.g., is 'price' a decimal?)
# 3. Automatically provide .create() and .update() methods
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'name', 'price', 'in_stock']
# --- How it's used in practice ---
# SERIALIZATION (Object -> JSON)
product = Product.objects.get(pk=1)
serializer = ProductSerializer(product)
print(serializer.data)
# Output:
# {'id': 1, 'name': 'Test Product', 'price': '100.00', 'in_stock': True}
# DESERIALIZATION (JSON -> Object)
json_data = {'name': 'New Product', 'price': '50.00'}
serializer = ProductSerializer(data=json_data)
if serializer.is_valid():
new_product = serializer.save()
print(f"Created: {new_product.name}")
# Output:
# Created: New Product
ViewSets and Routers are a set of conventions in DRF that let you write a full, working CRUD (Create, Read, Update, Delete) API with almost no code.
A `ViewSet` is a single class that combines the logic for a set of related views. Instead of writing a `ProductListView`, a `ProductDetailView`, and a `ProductCreateView`, you just write one `ProductViewSet`.
It doesn't have `.get()` or `.post()` methods. Instead, it has methods that map to database actions:`.list()`, `.retrieve()`, `.create()`, `.update()`, `.destroy()`.
A `Router` is what connects a `ViewSet` to your `urls.py` file. You tell the router about your ViewSet, and it automatically generates all the URL patterns for you.
# my_app/views.py
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
# This ONE class provides all 5 CRUD operations
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
# ...add permissions, authentication, etc.
# my_project/urls.py
from rest_framework.routers import DefaultRouter
from my_app.views import ProductViewSet
# 1. Create a router
router = DefaultRouter()
# 2. Register our ViewSet with the router
router.register(r'products', ProductViewSet, basename='product')
# 3. 'router.urls' now contains a full set of URLs
urlpatterns = [
# ... your other urls ...
path('api/', include(router.urls)),
]
# The router has now automatically generated:
# /api/products/ -> .list() (GET) and .create() (POST)
# /api/products/{pk}/ -> .retrieve() (GET), .update() (PUT),
# .partial_update() (PATCH),
# and .destroy() (DELETE)
Tip: The combination of `ModelSerializer`, `ModelViewSet`, and `Router` is the core of DRF. It lets you write a complete, production-ready JSON API for a model in as little as 3-5 lines of code.
2 questions · no sign-up, nothing stored
Progress is stored in this browser only - no sign-up, nothing sent anywhere.