Adding Django REST Framework (DRF) to an Existing Django Project
When you already have Django models powering your backend, Django REST Framework lets you expose that data as JSON endpoints with minimal disruption. The pragmatic goal is: reuse your existing models and business rules, serialize them safely, and publish endpoints that are authenticated, permissioned, paginated, and filterable.
Install and enable DRF
Add DRF (and optional filtering support) to your environment:
pip install djangorestframework django-filterThen enable it in settings.py:
INSTALLED_APPS = [ # ... "rest_framework", "django_filters",]Configure DRF defaults so every endpoint is not accidentally public and unpaginated. This is a good baseline for a real backend:
REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication", ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 20, "DEFAULT_FILTER_BACKENDS": [ "django_filters.rest_framework.DjangoFilterBackend", "rest_framework.filters.SearchFilter", "rest_framework.filters.OrderingFilter", ],}Why these defaults? IsAuthenticated prevents data leaks by default; pagination avoids returning thousands of rows; filter backends enable practical querying without custom code for every list endpoint.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Token authentication (practical option for APIs)
If you want token auth (common for mobile/SPA clients), add the token app and migrate:
INSTALLED_APPS = [ # ... "rest_framework.authtoken",]python manage.py migrateTo obtain tokens, you can use DRF’s built-in endpoint:
# project/urls.py (or api/urls.py)from rest_framework.authtoken.views import obtain_auth_tokenurlpatterns = [ # ... path("api/auth/token/", obtain_auth_token),]Clients then send Authorization: Token <token>. (Session auth can remain for internal/admin usage and browsable API convenience.)
Serializers: Turning Existing Models into Safe JSON
A serializer defines what fields are exposed, how relations are represented, and what validation happens on writes. Start with ModelSerializer to map directly from your existing models.
Example models (assumed to already exist)
Assume you already have something like Customer, Order, and OrderItem models in an app called sales. You will not change the models; you will only add API code around them.
Create serializers
Create sales/api/serializers.py:
from rest_framework import serializersfrom sales.models import Customer, Order, OrderItemclass CustomerSerializer(serializers.ModelSerializer): class Meta: model = Customer fields = ["id", "email", "full_name", "is_active", "created_at"] read_only_fields = ["id", "created_at"]class OrderItemSerializer(serializers.ModelSerializer): class Meta: model = OrderItem fields = ["id", "product_name", "unit_price", "quantity"] read_only_fields = ["id"]class OrderSerializer(serializers.ModelSerializer): customer = CustomerSerializer(read_only=True) customer_id = serializers.PrimaryKeyRelatedField( source="customer", queryset=Customer.objects.all(), write_only=True ) items = OrderItemSerializer(many=True, read_only=True) class Meta: model = Order fields = [ "id", "status", "total", "created_at", "customer", "customer_id", "items", ] read_only_fields = ["id", "created_at", "total"]Practical patterns used here:
customeris nested and read-only for convenient responses.customer_idis write-only so clients can create/update orders without sending nested customer objects.itemsis read-only to keep the first iteration simple; you can add item creation later via a dedicated endpoint.totalis read-only if it is computed server-side.
Expose only what you mean to expose
Do not use fields = "__all__" for production APIs. It can leak internal fields (flags, notes, cost price, audit fields) and makes future schema changes risky.
API Views: Start with ViewSets for CRUD, Add Custom Actions Only When Needed
For typical CRUD endpoints, ModelViewSet is the pragmatic default: list, retrieve, create, update, delete in one class. You can still lock down methods and add filtering/pagination.
Permissions that match real backend needs
Most backends need different access rules for different resources. Examples:
- Customers: staff-only or limited to internal roles.
- Orders: authenticated users can see their own orders; staff can see all.
Create sales/api/permissions.py:
from rest_framework.permissions import BasePermission, SAFE_METHODSclass IsStaffOrReadOnly(BasePermission): def has_permission(self, request, view): if request.method in SAFE_METHODS: return request.user and request.user.is_authenticated return request.user and request.user.is_staffclass IsOwnerOrStaff(BasePermission): def has_object_permission(self, request, view, obj): if request.user and request.user.is_staff: return True # assumes Order has a customer with a user relation or email mapping # adapt to your domain model return hasattr(obj, "user") and obj.user_id == request.user.idIf your Order is linked to a Customer which is linked to a User, adjust the ownership check accordingly (for example obj.customer.user_id).
Create viewsets with filtering, searching, ordering
Create sales/api/views.py:
from django_filters.rest_framework import FilterSet, filtersfrom rest_framework import viewsetsfrom rest_framework.permissions import IsAuthenticatedfrom sales.models import Customer, Orderfrom .serializers import CustomerSerializer, OrderSerializerfrom .permissions import IsStaffOrReadOnly, IsOwnerOrStaffclass OrderFilter(FilterSet): status = filters.CharFilter(field_name="status", lookup_expr="exact") created_from = filters.DateTimeFilter(field_name="created_at", lookup_expr="gte") created_to = filters.DateTimeFilter(field_name="created_at", lookup_expr="lte") class Meta: model = Order fields = ["status", "created_from", "created_to"]class CustomerViewSet(viewsets.ModelViewSet): queryset = Customer.objects.all().order_by("-created_at") serializer_class = CustomerSerializer permission_classes = [IsStaffOrReadOnly] search_fields = ["email", "full_name"] ordering_fields = ["created_at", "email"]class OrderViewSet(viewsets.ModelViewSet): serializer_class = OrderSerializer permission_classes = [IsAuthenticated, IsOwnerOrStaff] filterset_class = OrderFilter search_fields = ["id"] ordering_fields = ["created_at", "total"] def get_queryset(self): qs = Order.objects.select_related("customer").prefetch_related("items").order_by("-created_at") user = self.request.user if user.is_staff: return qs # adapt to your domain model; example if Order has a direct user FK: return qs.filter(user=user)Notes:
select_related/prefetch_relatedkeeps list endpoints fast when you include nested data.get_queryset()is the most common place to enforce “users can only see their own data”. Do not rely only on serializer logic for access control.filterset_classgives you stable, explicit filters rather than ad-hoc query params.
Wire Up URLs with a Router
Routers generate consistent REST-style URLs for viewsets. Create sales/api/urls.py:
from rest_framework.routers import DefaultRouterfrom .views import CustomerViewSet, OrderViewSetrouter = DefaultRouter()router.register(r"customers", CustomerViewSet, basename="customer")router.register(r"orders", OrderViewSet, basename="order")urlpatterns = router.urlsInclude these URLs in your project’s main URL configuration:
from django.urls import include, pathurlpatterns = [ # ... path("api/", include("sales.api.urls")),]This yields endpoints like:
GET /api/orders/(list)POST /api/orders/(create)GET /api/orders/{id}/(retrieve)PATCH /api/orders/{id}/(partial update)
Pagination: Practical Defaults and Client Usage
With PageNumberPagination enabled globally, list endpoints return a structure like:
{ "count": 125, "next": "https://example.com/api/orders/?page=2", "previous": null, "results": [ { "id": 1, "status": "paid", ... } ]}Clients request pages with ?page=2. If you need different page sizes per endpoint, you can create a custom pagination class and set pagination_class on a viewset.
Filtering, Searching, Ordering: What to Support and How to Use It
Filtering and ordering are where APIs become genuinely useful without custom endpoints for every query.
Filtering (explicit fields)
With the OrderFilter above, clients can do:
GET /api/orders/?status=paidGET /api/orders/?created_from=2026-01-01T00:00:00Z&created_to=2026-01-31T23:59:59Z
Search (text-like queries)
With search_fields set, clients can do:
GET /api/customers/?search=alice
Keep search fields limited and indexed where possible; search can be expensive on large tables.
Ordering
With ordering_fields set, clients can do:
GET /api/orders/?ordering=-created_atGET /api/orders/?ordering=total
Authentication and Permissions: A Practical Setup
Choose an authentication strategy per client type
| Client | Pragmatic choice | Why |
|---|---|---|
| Internal admin users in browser | SessionAuthentication | Uses existing login session; easy for staff tools |
| Mobile app / SPA | TokenAuthentication (or JWT) | Simple header-based auth; decoupled from cookies |
| Server-to-server | TokenAuthentication + scoped permissions | Easy rotation; can restrict endpoints |
If you later need JWT, you can swap token auth for a JWT package and keep your viewsets/permissions mostly unchanged.
Lock down write operations
Even if reads are allowed for authenticated users, writes often need stricter rules. Two common approaches:
- Per-view permissions: set
permission_classeson each viewset. - Per-action permissions: override
get_permissions()to require staff for destructive actions.
Example per-action permissions:
from rest_framework.permissions import IsAuthenticated, IsAdminUserclass CustomerViewSet(viewsets.ModelViewSet): # ... def get_permissions(self): if self.action in ["create", "update", "partial_update", "destroy"]: return [IsAdminUser()] return [IsAuthenticated()]Extending the API Without Rewriting It
Versioning approach (pick one and apply consistently)
Versioning is easiest when you decide early how URLs and clients will evolve. A pragmatic default is URL path versioning:
/api/v1/orders//api/v2/orders/
To implement, you can namespace your API URLs:
# project/urls.pyurlpatterns = [ # ... path("api/v1/", include("sales.api.urls")),]When you need a breaking change, create a new module (for example sales/api_v2/) and include it under /api/v2/. This keeps old clients working while you migrate.
Documentation options (practical choices)
Once endpoints exist, documentation becomes a product feature. Common options:
- OpenAPI/Swagger generation: add an OpenAPI schema generator and serve Swagger UI/Redoc for interactive docs.
- Human-written docs for critical flows: short “how to authenticate + key endpoints” pages for your frontend/mobile team.
Even with generated docs, keep a small set of examples for the most important endpoints (auth, list orders with filters, create order).
Where to add tests to protect critical endpoints
API tests should focus on access control, serialization shape, and key workflows. A practical structure:
sales/api/tests/test_orders_api.pysales/api/tests/test_customers_api.py
Use DRF’s test tools to validate authentication, permissions, and pagination:
from django.urls import reversefrom rest_framework.test import APITestCasefrom rest_framework import statusclass OrderApiTests(APITestCase): def test_list_requires_auth(self): url = reverse("order-list") # from router basename resp = self.client.get(url) self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) def test_user_sees_only_own_orders(self): # create two users + orders, authenticate as one, assert only own returned pass def test_pagination_shape(self): # create many orders, authenticate, assert resp.data has count/results keys passPrioritize tests for:
- Unauthorized access returns
401/403correctly. - Non-staff users cannot access other users’ objects (object-level permission).
- List endpoints are paginated and filterable as expected.
- Write endpoints reject invalid payloads and do not allow writing read-only fields.