Aiinfox Tech
Web Development

Django REST Framework Tutorial: Build Your First REST API

By AIInfoxTech7 min read

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 and POST /api/books/ creates one
  • GET /api/books/1/ returns a single book
  • PUT and PATCH /api/books/1/ update it in full or in part
  • DELETE /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_framework is missing from INSTALLED_APPS. Add it and restart the server.
  • no such table: books_book. A migration step was skipped. Run makemigrations books and then migrate.
  • 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 -u in 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 queryset attribute, usually after you switch to get_queryset(). Pass basename explicitly, 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:

  1. Filtering, searching and ordering on the list endpoint, so clients can ask for books by author or sort by date.
  2. Token or JWT authentication, because basic auth sends the password on every request and session auth does not suit mobile clients.
  3. Nested serializers for related models, for example a separate Author model with many books.
  4. Automated tests with APITestCase, so a change to the serializer cannot silently break a client.
  5. 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.

All articles

Request a Call Back

Need assistance or have questions? Simply fill out the form below, and one of our experts will get back to you as soon as possible. We’re here to help with all your queries and ensure you’re on the right path!

Request a Call Back