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_classes at 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.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India