Django REST Framework (DRF) is the de facto toolkit for building REST APIs on top of Django. It turns your Django models into JSON APIs with very little code, and gives you serializers for validation, a permissions and authentication layer, automatic pagination, filtering, and a browsable web API for testing — all built on Django's ORM, views and URL routing.
This guide is a hands-on introduction. You will set up DRF, write serializers, build the same CRUD endpoints with both function-based and class-based views, wire up ViewSets and routers, and add authentication, permissions, pagination and tests. The code targets Django 5.x, DRF 3.16+ and Python 3.12+, so it reflects current, idiomatic DRF rather than older patterns.
Prerequisites: a working knowledge of Django (models, views, URLs) and basic object-oriented Python.
What you will build
We will build a small users API with create, list, retrieve, update and delete endpoints. Along the way you will see two equivalent styles so you can pick whichever fits your project:
| Concept | Function-based | Class-based |
|---|---|---|
| Single action | @api_view(["GET"]) |
generics.ListAPIView |
| Full CRUD | several @api_view functions |
ModelViewSet + router |
| Best for | small, custom endpoints | standard CRUD, less boilerplate |
Most production APIs lean on class-based generics and ViewSets for the common cases, and drop down to function-based views for one-off, highly custom endpoints.
Install and configure DRF
Install DRF into your virtual environment:
pip install djangorestframeworkAdd rest_framework to INSTALLED_APPS in settings.py:
INSTALLED_APPS = [
# ...
"rest_framework",
"users", # the app we build below
# ...
]DRF ships with sensible defaults, but you will usually configure authentication, permissions and pagination globally in a single REST_FRAMEWORK dict in settings.py:
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,
}Define a model
We will expose a custom User model. (For a deeper dive on this pattern, see How to create a custom user model in Django.)
users/models.py
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
GENDER_CHOICES = (
("Male", "Male"),
("Female", "Female"),
("Other", "Other"),
)
class User(AbstractBaseUser, PermissionsMixin):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
username = models.CharField(max_length=100, blank=True, null=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
dob = models.DateField(null=True)
phone = models.CharField(max_length=20, null=True)
gender = models.CharField(choices=GENDER_CHOICES, max_length=6)
address = models.TextField(blank=True)
USERNAME_FIELD = "email"
def __str__(self):
return self.emailNote: AbstractBaseUser already provides the hashed password field, so you do not declare it on the model. Run python manage.py makemigrations && python manage.py migrate after defining the model.
Write serializers
Serializers do for APIs what forms do for HTML: they validate incoming data and convert model instances to and from JSON. DRF gives you a plain Serializer (like forms.Form) and a ModelSerializer (like forms.ModelForm) that infers fields from the model.
users/serializers.py
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
# write_only keeps the hashed password out of API responses
password = serializers.CharField(write_only=True, required=False)
class Meta:
model = User
fields = (
"id",
"first_name",
"last_name",
"email",
"username",
"dob",
"phone",
"gender",
"address",
"password",
)
def create(self, validated_data):
password = validated_data.pop("password", None)
user = User(**validated_data)
if password:
user.set_password(password)
user.save()
return user
def update(self, instance, validated_data):
password = validated_data.pop("password", None)
for attr, value in validated_data.items():
setattr(instance, attr, value)
if password:
instance.set_password(password)
instance.save()
return instanceBecause we hash the password in create()/update(), the same serializer handles both creating and editing users safely. write_only=True ensures the password hash is never returned in a response.
Build the views
DRF supports two styles. We will implement each CRUD action both ways so you can compare. A related deep-dive: Generic, function-based and class-based views in Django REST Framework.
Function-based views (@api_view)
Function views are explicit and easy to read for custom logic. The @api_view decorator wires up request parsing, the Response object and the browsable API.
users/views.py
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from .models import User
from .serializers import UserSerializer
@api_view(["GET", "POST"])
def users_list(request):
if request.method == "POST":
serializer = UserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
users = User.objects.all()
serializer = UserSerializer(users, many=True)
return Response(serializer.data)
@api_view(["GET", "PUT", "DELETE"])
def user_detail(request, pk):
user = get_object_or_404(User, pk=pk)
if request.method == "PUT":
serializer = UserSerializer(user, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
if request.method == "DELETE":
user.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
serializer = UserSerializer(user)
return Response(serializer.data)Two small but important details: always pass incoming data as UserSerializer(data=request.data) (not positionally), and prefer serializer.is_valid(raise_exception=True) so DRF returns a clean 400 with field errors automatically.
Class-based generic views
DRF's generic views collapse the boilerplate above into a few lines. Each maps to a standard CRUD shape.
users/generic_views.py
from rest_framework import generics
from .models import User
from .serializers import UserSerializer
class UserListCreateView(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = User.objects.all()
serializer_class = UserSerializerListCreateAPIView handles GET (list) and POST (create); RetrieveUpdateDestroyAPIView handles GET, PUT/PATCH and DELETE for a single object. Two classes cover the full CRUD surface.
ViewSets and routers — the least code
For standard CRUD, a ModelViewSet plus a router is the most concise option. The router generates all the URLs for you.
users/viewsets.py
from rest_framework import viewsets
from .models import User
from .serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializerURLs and routing
Django dropped django.conf.urls.url in 4.0 — use path() and re_path(). Here we register the function/generic views explicitly, and let a DefaultRouter generate the ViewSet routes.
users/urls.py
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from . import generic_views, views
from .viewsets import UserViewSet
router = DefaultRouter()
router.register(r"users", UserViewSet, basename="user")
urlpatterns = [
# ViewSet routes: /api/users/ and /api/users/<pk>/
path("api/", include(router.urls)),
# Function-based equivalents
path("fbv/users/", views.users_list, name="users-list"),
path("fbv/users/<int:pk>/", views.user_detail, name="user-detail"),
# Generic class-based equivalents
path("cbv/users/", generic_views.UserListCreateView.as_view(), name="cbv-users-list"),
path("cbv/users/<int:pk>/", generic_views.UserDetailView.as_view(), name="cbv-user-detail"),
]Authentication
DRF supports several authentication schemes out of the box; you set them globally (as above) or per-view via authentication_classes.
- SessionAuthentication — reuses Django's login session; great for the browsable API and same-site frontends.
- TokenAuthentication — a simple per-user token sent in the
Authorization: Token <key>header. Addrest_framework.authtokentoINSTALLED_APPS, migrate, then expose anobtain_auth_tokenendpoint. See How to implement token-based authentication in DRF. - JWT — for stateless, expiring tokens (common for SPAs and mobile apps), use the
djangorestframework-simplejwtpackage, which adds access/refresh token endpoints.
urls.py (token endpoint)
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns += [
path("api/token/", obtain_auth_token, name="api-token"),
]Permissions
Authentication answers "who are you?"; permissions answer "are you allowed to do this?". Common built-ins include AllowAny, IsAuthenticated, IsAdminUser and IsAuthenticatedOrReadOnly. Set them globally or per view:
from rest_framework import generics
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import User
from .serializers import UserSerializer
class UserListCreateView(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAuthenticatedOrReadOnly]For row-level rules ("users can only edit their own record"), subclass BasePermission and implement has_object_permission().
Pagination and filtering
With DEFAULT_PAGINATION_CLASS and PAGE_SIZE set in settings, list endpoints are paginated automatically and return count, next, previous and results. For filtering and search, install django-filter and configure backends:
# pip install django-filter
# settings.py
REST_FRAMEWORK = {
# ...
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
}
# views.py
class UserListCreateView(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
filterset_fields = ["gender", "is_active"]
search_fields = ["first_name", "last_name", "email"]
ordering_fields = ["first_name", "dob"]Clients can then call GET /cbv/users/?gender=Female&search=alice&ordering=first_name.
The browsable API
One of DRF's most loved features: visit any endpoint in a browser and you get an interactive, HTML-rendered version of the API with forms to test POST/PUT and links to navigate. It is enabled by default in development via the BrowsableAPIRenderer and is invaluable for debugging without Postman or curl. For production-only JSON, restrict renderers to JSONRenderer.
Testing your API
DRF provides APITestCase and APIClient, which behave like Django's test client but understand DRF requests and responses.
users/tests.py
from rest_framework import status
from rest_framework.test import APITestCase
from .models import User
class UserAPITests(APITestCase):
def test_create_user(self):
payload = {
"first_name": "Alice",
"last_name": "Doe",
"email": "alice@example.com",
"gender": "Female",
"password": "s3cret-pass",
}
response = self.client.post("/fbv/users/", payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(User.objects.count(), 1)
# password must never be echoed back
self.assertNotIn("password", response.data)
def test_list_users(self):
User.objects.create(email="bob@example.com", first_name="Bob", gender="Male")
response = self.client.get("/fbv/users/")
self.assertEqual(response.status_code, status.HTTP_200_OK)Run the suite with python manage.py test.
DRF vs FastAPI — which to choose?
Both are excellent; the right pick depends on what you already have and what you are building.
- Choose DRF when you are already on Django (or want its admin, ORM, auth and batteries-included ecosystem), are exposing an API over a relational data model, or want the browsable API and a mature, opinionated CRUD toolkit. It is the path of least resistance for adding an API to an existing Django app.
- Choose FastAPI for greenfield, async-heavy or high-throughput services, when you want first-class
async/await, automatic OpenAPI docs out of the box, and Pydantic-based typing — especially for ML/AI inference endpoints and microservices that do not need Django's full stack.
In practice, many teams run Django + DRF for the core product and FastAPI for specialised async or AI services. If you are weighing the two, our FastAPI vs DRF comparison guidance and our Python team can help you decide.
Frequently Asked Questions
What is Django REST Framework used for?
Django REST Framework is used to build REST APIs on top of Django. It handles serialization (model-to-JSON and back), request validation, authentication, permissions, pagination and filtering, and provides a browsable web API for testing — letting you turn Django models into production HTTP APIs with minimal code.
Should I use function-based or class-based views in DRF?
Use class-based generic views or ViewSets for standard CRUD — they remove boilerplate and a ModelViewSet plus a router can implement full CRUD in a few lines. Use function-based @api_view views for small, highly custom endpoints where explicit, readable logic matters more than reuse. Most production APIs mix both.
What is the difference between a Serializer and a ModelSerializer?
A Serializer is like forms.Form: you declare every field and write your own create()/update() logic. A ModelSerializer is like forms.ModelForm: it infers fields from a model's definition and generates create()/update() automatically. Use ModelSerializer for model-backed APIs and a plain Serializer for custom, non-model payloads.
How do I add authentication to a DRF API?
Set DEFAULT_AUTHENTICATION_CLASSES in the REST_FRAMEWORK settings dict, or authentication_classes per view. DRF ships with session and token authentication; for stateless tokens common in SPAs and mobile apps, add djangorestframework-simplejwt for JWT access/refresh tokens. Pair authentication with permission classes such as IsAuthenticated.
Which Django and Python versions does DRF support in 2026?
The DRF 3.16/3.17 line (2026) supports current Django 5.x and 6.0 and Python 3.10 through 3.14. The examples in this guide target Django 5.x, DRF 3.16+ and Python 3.12+, which is a safe, widely compatible baseline for new projects.
When should I use FastAPI instead of DRF?
Reach for FastAPI on greenfield, async-heavy or high-throughput services that benefit from native async/await, automatic OpenAPI docs and Pydantic typing — for example AI/ML inference endpoints and lightweight microservices. Stay with DRF when you are already on Django or want its admin, ORM and batteries-included ecosystem around your API.
Next steps
You now have the full picture: install and configure DRF, write serializers, build CRUD with function-based views, generic class-based views and ViewSets, route with path() and routers, and layer on authentication, permissions, pagination, filtering and tests. From here, explore the official DRF tutorial and our open-source Django CRM for a real-world DRF codebase.
MicroPyramid has built and maintained Django and DRF APIs for over a decade across startups and enterprises. If you need help designing, building or modernising an API, see our Django development services.