This Django REST Framework tutorial takes you from an empty folder to a working REST API. You will create a project and an app, define a Book model, expose it through a serializer and a viewset, register the routes, test the endpoints in the browser and with curl, then add pagination and a permission rule. Every code block is complete and runs on Django 5 with Django REST Framework 3.15.
The only assumptions are that you have Python 3.10 or later installed and that you have seen a Django model before. If you have not, the Python Django developer roadmap covers the ground you need before this post.
What a REST API is, in one paragraph
A REST API is a set of URLs that accept and return data, usually JSON, rather than HTML pages. A client sends an HTTP request (GET to read, POST to create, PUT or PATCH to update, DELETE to remove) and the API replies with a status code and a body. Django REST Framework (DRF) turns Django models into these endpoints with very little code.
Step 1: Set up the project and app
Create a folder and a virtual environment, then install the two packages. The trailing dot on startproject keeps manage.py at the top level instead of nesting it one folder deeper.
mkdir library-api
cd library-api
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on a non-POSIX shell
pip install django djangorestframework
django-admin startproject library .
python manage.py startapp books
Open library/settings.py and register both the framework and your app. Without rest_framework in this list the browsable API cannot find its templates, which is the first error most beginners hit.
# library/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"books",
]
Step 2: Define the Book model
The model is the single source of truth for what a book looks like. Keep it small for now. You can add fields later and the serializer will pick them up.
# books/models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=120)
isbn = models.CharField(max_length=13, unique=True)
published_on = models.DateField()
in_stock = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return f"{self.title} ({self.isbn})"
The ordering option matters more than it looks. Pagination needs a stable order, and Django will warn about an unordered queryset the moment you paginate one. Now create the table and an admin user you will use for protected requests later.
python manage.py makemigrations books
python manage.py migrate
python manage.py createsuperuser
Step 3: Write a ModelSerializer
A serializer does two jobs: it converts model instances to JSON on the way out and validates incoming JSON on the way in. ModelSerializer reads the field types from the model, so you only declare which fields to expose and any extra validation.
# books/serializers.py
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ["id", "title", "author", "isbn", "published_on", "in_stock", "created_at"]
read_only_fields = ["id", "created_at"]
def validate_isbn(self, value):
if not (len(value) == 13 and value.isdigit()):
raise serializers.ValidationError("Enter a 13-digit ISBN without hyphens.")
return value
Any method named validate_<field> runs automatically for that field, after the validators copied from the model such as the length limit and the uniqueness check. A bad value produces a 400 response with a clear message rather than a database error, and whatever the method returns is what gets saved.
Step 4: Build a ModelViewSet
A viewset groups the list, create, retrieve, update and delete actions for one resource into a single class. ModelViewSet implements all of them, so your first version is two lines of real code.
# books/views.py
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
The framework handles content negotiation, parsing, validation errors and status codes. You override only the parts that differ for your project.
Step 5: Register routes with DefaultRouter
Routers generate the URL patterns for a viewset. DefaultRouter also adds an API root view at the base path, which is handy while exploring.
# books/urls.py
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register("books", BookViewSet, basename="book")
urlpatterns = router.urls
# library/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("books.urls")),
path("api-auth/", include("rest_framework.urls")),
]
The api-auth/ line adds login and logout links to the browsable API so you can test authenticated requests without leaving the browser. With this in place the router gives you:
GET /api/books/lists books andPOST /api/books/creates oneGET /api/books/1/returns a single bookPUTandPATCH /api/books/1/update it in full or in partDELETE /api/books/1/removes it
Step 6: Test with the browsable API and curl
Start the server and open http://127.0.0.1:8000/api/books/ in a browser.
python manage.py runserver
DRF renders an HTML page showing the JSON response and a form for creating records. Log in with the superuser at the top right, fill in the form and submit.
From a second terminal, run the same operations with curl to see exactly what a client sends and receives. The -u flag uses basic authentication, which DRF enables by default.
curl -u admin:yourpassword -X POST http://127.0.0.1:8000/api/books/ \
-H "Content-Type: application/json" \
-d '{"title": "Learning Django", "author": "Example Author", "isbn": "9780000000001", "published_on": "2024-01-15"}'
{
"id": 1,
"title": "Learning Django",
"author": "Example Author",
"isbn": "9780000000001",
"published_on": "2024-01-15",
"in_stock": true,
"created_at": "2026-09-08T03:30:00.412345Z"
}
Send the same request again and you get a 400 telling you a book with that ISBN already exists. Send a 10-digit ISBN and the 400 carries the message from validate_isbn. Now read, update and delete:
curl http://127.0.0.1:8000/api/books/
curl -u admin:yourpassword -X PATCH http://127.0.0.1:8000/api/books/1/ \
-H "Content-Type: application/json" -d '{"in_stock": false}'
curl -u admin:yourpassword -X DELETE http://127.0.0.1:8000/api/books/1/ -i
A successful DELETE returns 204 No Content with an empty body, which is why the -i flag is there to print the headers.
Step 7: Add pagination and a simple permission class
Two settings turn the list endpoint into pages of ten. This is a global default, so every viewset you add later inherits it.
# library/settings.py
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}
The list response changes shape. Records now sit under a results key alongside count, next and previous, and clients fetch the next page with ?page=2.
Right now anyone can create or delete books. A permission class is a small object with a has_permission method that returns True or False for each request. This one allows reads for everyone and writes for staff users only.
# books/permissions.py
from rest_framework import permissions
class IsStaffOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return bool(request.user and request.user.is_staff)
# books/views.py
from rest_framework import viewsets
from .models import Book
from .permissions import IsStaffOrReadOnly
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [IsStaffOrReadOnly]
SAFE_METHODS is the tuple of GET, HEAD and OPTIONS. Repeat the POST from earlier without the -u flag and you get 403 Forbidden. Add it back and the request succeeds. That difference is the whole idea of a permission class.
Common errors in this Django REST Framework tutorial and how to fix them
- TemplateDoesNotExist: rest_framework/api.html.
rest_frameworkis missing fromINSTALLED_APPS. Add it and restart the server. - no such table: books_book. A migration step was skipped. Run
makemigrations booksand thenmigrate. - 404 on /api/books. The router generates URLs with a trailing slash, so request
/api/books/. A GET without the slash may redirect, but a POST will not. - 403 Forbidden on a write. With the permission class in place, the request is anonymous or the user is not staff. Pass
-uin curl or log in through the browsable API. - CSRF Failed: CSRF cookie not set. A script sent a session cookie without the CSRF token. Use basic or token authentication from scripts and keep session login for the browser.
- 400 with a JSON body of field names. This is validation, not a bug. Each key names the field that failed and the message says why, for example a duplicate ISBN.
- 'basename' argument not specified. The router could not infer a name because the viewset has no
querysetattribute, usually after you switch toget_queryset(). Passbasenameexplicitly, as the code above already does.
What to learn next
You now have a working API with validation, pagination and access control. The next topics, roughly in the order you will need them:
- Filtering, searching and ordering on the list endpoint, so clients can ask for books by author or sort by date.
- Token or JWT authentication, because basic auth sends the password on every request and session auth does not suit mobile clients.
- Nested serializers for related models, for example a separate Author model with many books.
- Automated tests with
APITestCase, so a change to the serializer cannot silently break a client. - Throttling and an OpenAPI schema once other people start calling your API.
If you are still deciding which framework to commit to, Django vs Flask: which to learn first explains why Django with DRF is the usual starting point for API work. To build the front end that consumes an endpoint like this one, the MERN full stack development programme covers the client side.
The Python and Django web development programme covers this stack in full, from models and REST APIs through testing and deployment, with project work throughout. If you want advice on where you are in that path, contact AIInfoxTech and we will talk it through with you.
DjangoREST APIPythonWeb DevelopmentBack-end
Frequently asked questions
Do I need to know Django before starting this Django REST Framework tutorial?
You should be comfortable with the basics: creating a project and an app, defining a model and running migrations. Everything specific to DRF, including serializers, viewsets and routers, is explained in the post.
What is the difference between a serializer and a model?
The model defines how data is stored in the database. The serializer defines how that data is presented to and accepted from API clients, including validation of incoming JSON.
Why does my POST request return 403 Forbidden?
Once a permission class restricts writes, an anonymous request or a non-staff user is rejected. Send credentials with curl using the -u flag, or log in through the browsable API before submitting the form.
Should I use ModelViewSet or write separate API views?
ModelViewSet is the fastest route when an endpoint maps cleanly onto one model with standard create, read, update and delete actions. Write generic or plain API views when the behaviour does not fit that pattern.
How does pagination change the response?
With PageNumberPagination enabled, a list response becomes an object with count, next, previous and results keys instead of a bare array. Clients request further pages with a page query parameter.
Learn this in a classroom in Mohali
Mentor-led batches, real projects and placement support. Talk to the team about the programme this article belongs to.
