Building a REST API with DRF
Django REST Framework (DRF) is the standard way to expose Django models as a JSON API. Install it with
pip install djangorestframework and add "rest_framework" to INSTALLED_APPS.
The four moving parts
| Piece | Job |
|---|---|
| Model | The database table |
| Serializer | Converts model instances to JSON and validates incoming JSON |
| ViewSet | Groups list / create / retrieve / update / destroy in one class |
| Router | Generates the URL patterns for a viewset |
The model
# models.py
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=120)
price = models.DecimalField(max_digits=10, decimal_places=2)
in_stock = models.BooleanField(default=True)
def __str__(self):
return self.name
Serializer and viewset
# serializers.py
from rest_framework import serializers
from .models import Product
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["id", "name", "price", "in_stock"]
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Price must be greater than zero.")
return value
# views.py
from rest_framework import viewsets, permissions, filters
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all().order_by("id")
serializer_class = ProductSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ["name"]
ordering_fields = ["price", "name"]
# urls.py
from rest_framework.routers import DefaultRouter
from .views import ProductViewSet
router = DefaultRouter()
router.register("products", ProductViewSet)
urlpatterns = router.urls
Those few lines already give you GET /products/, POST /products/, GET /products/7/, PUT, PATCH and
DELETE, plus a browsable HTML interface when you open the URL in a browser.
Settings worth adding on day one
# settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 25,
"DEFAULT_THROTTLE_RATES": {"anon": "60/hour", "user": "1000/hour"},
}
Without pagination, a /products/ call on a catalogue of 80,000 textile SKUs will try to serialise every row and
time out.
Trying it out
python manage.py runserver
curl http://127.0.0.1:8000/products/?search=saree
curl -X POST http://127.0.0.1:8000/products/ \
-H "Content-Type: application/json" \
-H "Authorization: Token 9f2c4ab..." \
-d '{"name": "Cotton Saree", "price": "1299.00", "in_stock": true}'
A validation failure returns 400 with a per-field error dict, which is exactly what a front-end needs.
Nested and computed fields
class InvoiceSerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name", read_only=True)
gst_amount = serializers.SerializerMethodField()
class Meta:
model = Invoice
fields = ["id", "customer_name", "amount", "gst_amount"]
def get_gst_amount(self, obj):
return round(obj.amount * 18 / 100, 2)
Common mistakes
- Forgetting
order_by()on the queryset, which makes pagination return inconsistent pages. - Leaving
permission_classesat the default so anyone can delete rows. - Serialising related objects without
select_related/prefetch_related, creating an N+1 query storm. - Exposing every model field, including internal flags and cost prices.
Practice
Build a Customer model with name, city and GSTIN, expose it through a serializer, viewset and router, add
search on name and city, paginate at 10 per page, and confirm with curl that an unauthenticated POST is
rejected while a GET still works.