django rest framework tutorialdjango rest apipython api guidedrf viewsetsdjango serializers

Django REST Framework Tutorial to Build APIs Fast

A practical Django REST Framework tutorial. Learn to build powerful APIs using serializers, viewsets, and secure authentication with real-world examples.

42 Coffee Cups Team
23 min read
Django REST Framework Tutorial to Build APIs Fast

If you're looking to build powerful APIs with Django, you've come to the right place. This guide will walk you through everything you need to know about the Django REST Framework (DRF), taking you from initial setup to serialization and security.

DRF is a must-have toolkit that sits on top of Django. Its whole purpose is to make creating web APIs radically simpler, letting you build scalable and secure endpoints with a tiny amount of code. For any developer who needs to bridge the gap between a backend service and a frontend application, DRF is almost always the best tool for the job.

Why DRF Is a Game-Changer for API Development

Image

Before we jump into the code, let's talk about why the Django REST Framework is so popular in the first place. At its heart, Django is a fantastic Python web framework built for rapidly developing secure, full-featured websites. But its main strength lies in building traditional, server-rendered web pages, not APIs.

That's where DRF steps in. It’s a powerful and flexible library that extends Django's core functionality, giving it all the tools needed to build top-notch RESTful APIs.

A good way to think about it is this: if Django is the foundation and walls of your house, DRF is the specialized toolkit for all the wiring and plumbing needed to connect it to the outside world.

Django vs. Django REST Framework Core Functions

To make it crystal clear, let's break down what each framework handles. Core Django manages the web application's foundation, while DRF specializes in the API-specific tasks that sit on top of that foundation.

FeatureCore DjangoDjango REST Framework (DRF)
Primary RoleBuilds web applications with HTML templatesBuilds web APIs that serve data (like JSON)
Data HandlingORM for database interactionSerializers for converting data to/from JSON
ViewsRenders HTML templates for browsersCreates API endpoints for clients
RoutingMaps URLs to view functionsAutomatically generates API URLs from ViewSets
AuthenticationManages user sessions for web pagesHandles API authentication (Tokens, OAuth)

Ultimately, DRF takes care of the repetitive, complex parts of API development so you can focus on what makes your application unique.

The Magic of Serialization

One of DRF's most powerful features is its serialization system. Serializers are what turn your complex data, like Django model instances from your database, into simple Python data types that can be easily rendered into JSON for your API.

They also work in reverse—a process called deserialization—by taking incoming JSON data, validating it, and converting it back into complex types you can save to your database. This automation is a massive time-saver and eliminates a ton of boilerplate code. Without it, you'd be stuck manually converting and validating data for every single API endpoint, which is both tedious and a recipe for bugs.

Streamlined Views and Security

DRF also gives you tools like class-based views, ViewSets, and Routers that cut out the repetitive work of building CRUD (Create, Read, Update, Delete) operations. Instead of writing a separate view for every action, you can define a single ViewSet to handle it all. A Router will then automatically generate the necessary URL patterns for you. It's incredibly efficient.

On top of that, DRF comes packed with features that are essential for any real-world API:

  • Authentication: Out-of-the-box support for session-based, token-based, OAuth1, and OAuth2 authentication.
  • Permissions: Fine-grained control over who can see or change specific resources.
  • Browsable API: A clean, user-friendly HTML interface that lets you interact with your API directly in the browser—perfect for testing and debugging.

The real value of DRF is that it lets you focus on your application's business logic instead of reinventing the wheel for common API functionalities. It provides a solid, well-tested foundation for building scalable and maintainable APIs.

The framework's popularity isn't just word-of-mouth. According to PyPI statistics, DRF gets around 14.4 million downloads a month, which is more than half of Django's total downloads. That number alone shows just how many developers rely on it for their projects.

Of course, choosing the right backend framework is a big decision. While DRF is the perfect partner for Django, it's always smart to know your options. You can see how Django stacks up against other major players in our https://www.42coffeecups.com/blog/django-vs-rails comparison. Understanding the broader trends in software engineer demand also helps put into perspective why mastering a specialized, in-demand tool like DRF is such a smart career move.

Setting Up Your Project and First Endpoint

A diagram showing API integration between a server and various client applications like mobile and web.

Alright, enough theory. The best way to really get a feel for this stuff is to roll up our sleeves and build something. We’re about to go from an empty folder to a working API endpoint that you can hit from your browser.

Starting with good habits will save you a world of pain later. First on that list is using a virtual environment. Think of it as a clean, isolated sandbox for our project, keeping its dependencies from messing with any other Python projects on your machine.

Initializing the Project Environment

First things first, let's make a home for our project. Open up your terminal, create a new directory, and jump inside it.

mkdir my_drf_project && cd my_drf_project

Now, we'll create and activate that virtual environment. For macOS or Linux, you'll run:

python3 -m venv venv source venv/bin/activate

You should see (venv) pop up in your terminal prompt, which means you're good to go. With our sandbox active, we can safely install Django and Django REST Framework.

pip install django djangorestframework

This command grabs the latest stable versions for us. Once that's done, we can create the Django project itself and a dedicated app for our API. It's a common practice to keep API logic in its own app to keep things organized.

  • Create the Django Project: This command scaffolds out the main project files. The . at the end is important—it tells Django to create the project in the current directory. django-admin startproject core .
  • Create the API App: This is where we'll put our models, views, and serializers. python manage.py startapp api

Configuring Django for DRF

Before we can use DRF, we need to let Django know it exists. This is a quick but absolutely critical tweak to your project settings.

Open up core/settings.py and find the INSTALLED_APPS list. You need to add rest_framework and the api app we just created.

core/settings.py

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles',

# Add these two lines
'rest_framework',
'api',

]

This simple change unlocks all the powerful features of DRF, like its views and serializers, making them available across our project. There's a lot more you can do with Django configurations, and if you want another perspective, you can find a solid overview of building an API with Django on the 42coffeecups.com blog.

Building Your First API Endpoint

Now for the fun part—let's make a live endpoint. We'll start with something dead simple: an endpoint that just returns a fixed JSON message. This is a great way to confirm our entire setup is working before we get into the complexity of databases and serializers.

First, we need to define a view. In Django, a view is just a function or class that handles a web request and spits out a response. We'll use DRF's APIView class, which is a perfect base for building custom endpoints.

Pop open api/views.py and add this code:

api/views.py

from rest_framework.views import APIView from rest_framework.response import Response

class FirstEndpointView(APIView): def get(self, request, *args, **kwargs): """ This view returns a simple hardcoded JSON response. """ data = { 'message': 'Hello, world!', 'status': 'success' } return Response(data)

Here, our class has a get method that will handle any HTTP GET request sent to its URL. The Response object from DRF is smart enough to take our Python dictionary and automatically render it as a proper JSON response.

Key Takeaway: Getting comfortable with APIView and Response is fundamental. These tools handle the boring parts of dealing with HTTP content types so you can just focus on the data you want your API to send.

Finally, we have to wire this view up to a URL. Create a new file, api/urls.py, and add the route for our view.

api/urls.py

from django.urls import path from .views import FirstEndpointView

urlpatterns = [ path('hello/', FirstEndpointView.as_view(), name='hello'), ]

The last piece of the puzzle is telling our main project about these new API-specific URLs. Head over to core/urls.py and make it look like this:

core/urls.py

from django.contrib import admin from django.urls import path, include

urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('api.urls')), ]

That's it! Fire up the development server.

python manage.py runserver

Now, point your web browser to http://127.0.0.1:8000/api/hello/. You should see your very first API response, beautifully formatted by DRF’s handy browsable API. Success

Mastering Serializers for Clean Data Handling

A flowchart showing how serializers convert complex data types like Django models into JSON format for APIs.

If views are the brains of a Django REST Framework API, then serializers are its heart. They're the workhorses that translate your data between complex Python objects, like Django model instances, and simple, frontend-friendly formats like JSON.

Think of a serializer as a bilingual interpreter. It ensures your backend database and a frontend JavaScript application can communicate flawlessly.

This translation goes both ways. When a client requests data, the serializer takes your model and turns it into a dictionary, which DRF then renders as JSON. When a client sends data—say, in a POST request to create a new blog post—the serializer validates the incoming JSON and converts it back into a model instance you can save. This two-way process is called serialization and deserialization, and getting it right is fundamental to building a solid API.

Creating Your First ModelSerializer

The fastest way to get up and running is with DRF's ModelSerializer. This class is a thing of beauty. It automatically inspects your Django model and generates the corresponding serializer fields for you, saving you a ton of boilerplate code. It’s a perfect example of the "Don't Repeat Yourself" (DRY) principle in action.

Let's imagine we have a simple Post model for a blog:

api/models.py

from django.db import models from django.contrib.auth.models import User

class Post(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE) title = models.CharField(max_length=200) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
    return self.title

To create a serializer for this, we'll make a new file, api/serializers.py, and drop in this code. It's a standard convention that helps keep projects tidy.

api/serializers.py

from rest_framework import serializers from .models import Post

class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'content', 'author', 'created_at']

And that’s it! The ModelSerializer will automatically create fields for id, title, content, author, and created_at right from the Post model. The nested Meta class simply tells the serializer which model to look at and which fields to include.

Customizing Serializer Fields

The default ModelSerializer is incredibly powerful, but you'll almost always need to tweak its behavior. Maybe you need to control which fields are readable or writable, or perhaps add custom validation to protect your data's integrity.

For instance, a user should never be able to change the author or created_at timestamp of a post after it's been created. We can lock this down by making those fields read-only.

api/serializers.py

class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'content', 'author', 'created_at'] read_only_fields = ['author', 'created_at']

By adding the read_only_fields attribute, we tell DRF to include author and created_at in API responses but to completely ignore them in incoming requests. This is a simple but crucial step to prevent clients from meddling with data they shouldn't control.

Pro Tip: Using read_only_fields is a simple yet effective security measure. Get into the habit of explicitly marking any field that shouldn't be user-editable, like timestamps, user associations, or calculated properties.

Implementing Custom Validation

Validation is another critical job for serializers. While DRF handles basic validation out of the box (like checking a CharField's max length), you'll often need to enforce more complex business rules.

Let's say our blog has a quirky rule: the title and content of a post can't be identical. We can enforce this with a custom validate method right in our serializer.

api/serializers.py

from rest_framework import serializers from .models import Post

class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'content', 'author', 'created_at'] read_only_fields = ['author', 'created_at']

def validate(self, data):
    """
    Check that the title and content are different.
    """
    if data['title'] == data['content']:
        raise serializers.ValidationError("Title and content cannot be the same.")
    return data

Now, if a client tries to create or update a post where the title and content match, the API will automatically reject it with a clean 400 Bad Request error. This kind of object-level validation is perfect for rules that depend on multiple fields at once. If you just need to validate a single field, you can also write a validate_<field_name> method for more focused logic. This is where you really start to see how powerful serializers can be.

Building Scalable APIs with ViewSets and Routers

A visual representation of DRF's ViewSets and Routers automatically handling API routing and CRUD operations.

Once you've got the hang of serializers, you'll quickly realize that writing individual views for every single action (list, create, update, etc.) gets old. Fast. It's a ton of boilerplate code that feels incredibly repetitive. This is exactly the problem that ViewSets and Routers were designed to solve.

Think of a ViewSet as a single class that bundles together all the related logic for a resource. Instead of creating a PostListView, a PostDetailView, and a PostCreateView, you just create one PostViewSet. This is pure Django philosophy—Don't Repeat Yourself (DRY).

Refactoring to a ModelViewSet

The real magic happens with the ModelViewSet. It's the workhorse of DRF and does for views what ModelSerializer does for data representation. It automatically wires up all the standard CRUD (Create, Read, Update, Delete) operations for a given model. You get a massive amount of functionality for very little code.

Let’s refactor what we’ve built so far. We can gut our old APIView and replace it with just a handful of lines.

In api/views.py, swap out the old code with this:

api/views.py

from rest_framework import viewsets from .models import Post from .serializers import PostSerializer

class PostViewSet(viewsets.ModelViewSet): """ A viewset for viewing and editing post instances. """ queryset = Post.objects.all() serializer_class = PostSerializer

And that’s it. Seriously. This one small class now gives us a full set of endpoints:

  • GET /api/posts/ to list all posts
  • POST /api/posts/ to create a new post
  • GET /api/posts/{id}/ to retrieve a single post
  • PUT /api/posts/{id}/ to update a post
  • PATCH /api/posts/{id}/ to partially update a post
  • DELETE /api/posts/{id}/ to delete a post

By simply pointing the ModelViewSet to a queryset and a serializer_class, it handles all the database interaction and response logic for you.

Automating URLs with Routers

Okay, we have a powerful ViewSet, but it's not connected to any URLs yet. You could wire up each URL pattern manually, but why would you? DRF gives us an even better tool: Routers.

A router automatically scans your ViewSet and generates all the URL patterns for you. It's a huge time-saver and guarantees your API follows standard RESTful conventions without you having to think about it.

Let's put one in place. In your api/urls.py file, set up a DefaultRouter:

api/urls.py

from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import PostViewSet

Create a router and register our viewset with it.

router = DefaultRouter() router.register(r'posts', PostViewSet, basename='post')

The API URLs are now determined automatically by the router.

urlpatterns = [ path('', include(router.urls)), ]

The router.register() line is where the action is. It takes our PostViewSet, hooks it up to the /posts/ URL prefix, and builds out all the routes we listed above. Your urls.py file stays clean and simple, even as your API grows.

This combination of ModelViewSet and DefaultRouter is the bread and butter of DRF development. You can get a fully functional, REST-compliant API for a model up and running in under 10 lines of code.

This kind of rapid development is a big reason why Django remains so popular. The Django Developers Survey 2025 revealed that 62% of developers use it for full-stack work, and 59% choose it for new projects. You can dive deeper into the stats by checking out the full Django developer report from JetBrains.

When to Use APIView vs ViewSet

ViewSets are incredible for standard model-based APIs, but they aren't the answer to everything. Sometimes you need more fine-grained control, and that's when you should reach for the more basic APIView.

Knowing when to use which is a mark of an experienced developer. Here’s a quick cheat sheet:

Use CaseRecommended ClassWhy?
Standard CRUD on a modelModelViewSetIt's the fastest path to a complete API for a model. Maximum productivity.
Custom, non-CRUD endpointAPIViewGives you complete control. Perfect for one-off actions like "send-email" or "process-payment".
Read-only model dataReadOnlyModelViewSetA lean ViewSet that only provides the list and retrieve actions. Great for security.

Using the right tool for the job makes your application so much easier to maintain. Stick to ViewSets for your typical resource management and drop down to APIView for anything that breaks the mold. This keeps your code predictable and easy for you (and your team) to navigate later on.

Getting API Security Right

Building a slick API is one thing, but leaving it unsecured is like leaving the front door of your house wide open. It's not just a technical oversight; it's a huge liability. Thankfully, this is where the Django REST Framework really comes through, providing a solid, pluggable system for security that handles the heavy lifting for you.

This "batteries-included" approach is a big reason why so many developers stick with the framework. In fact, a recent JetBrains survey on the State of Python revealed that 42% of Python web developers choose Django for their backend. You can get more insights on Python's continued dominance in web development on bybowu.com.

At its heart, API security in DRF boils down to a two-part question:

  1. Authentication: Who is this person? This is all about identifying the user behind the request.
  2. Permissions: Are they allowed to do that? Once you know who they are, you check if they have the right to perform a specific action.

Handling Authentication

DRF offers a few ways to handle authentication, but two stand out for most modern apps: SessionAuthentication and TokenAuthentication. The one you pick really just depends on what kind of client will be talking to your API.

  • SessionAuthentication: This is your go-to if your API is being used by a JavaScript frontend served from the same Django project. It piggybacks on Django's standard session system, using secure HTTP cookies. It's simple and tightly integrated.
  • TokenAuthentication: This is perfect for clients that can't easily maintain a session, like a mobile app or a separate single-page application. The client just needs to send a unique token in the request header to prove who they are.

Let's get token authentication set up. First, you'll need to add rest_framework.authtoken to your INSTALLED_APPS setting and then run python manage.py migrate. This command creates the database table that DRF needs to store the tokens.

With that done, we can now lock down our PostViewSet by telling it which authentication methods to use.

api/views.py

from rest_framework import viewsets from rest_framework.authentication import SessionAuthentication, TokenAuthentication from rest_framework.permissions import IsAuthenticated from .models import Post from .serializers import PostSerializer

class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer authentication_classes = [SessionAuthentication, TokenAuthentication] permission_classes = [IsAuthenticated]

Just by adding those two lines, our API is no longer a free-for-all. Any request that comes in without proper authentication will get a 401 Unauthorized error.

Controlling Access with Permissions

Okay, so we've identified the user. Now what? Authentication confirms who someone is, but permissions determine what they can actually do. The IsAuthenticated permission class is a basic but effective gatekeeper: if you're logged in, you're in. If not, you're out.

But real-world applications need more nuance. For example, you probably want to let anyone view a post, but only allow the original author to edit or delete it. This is the perfect job for a custom permission class.

Let’s build one. Create a new file in your app called api/permissions.py:

api/permissions.py

from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission): """ Custom permission to only allow authors of an object to edit it. """ def has_object_permission(self, request, view, obj): # Anyone can read, so we'll always allow GET, HEAD, or OPTIONS. if request.method in permissions.SAFE_METHODS: return True

    # Write permissions are only granted to the post's author.
    return obj.author == request.user

This little class is quite smart. It first checks if the request is a "safe" method (like GET). If it is, access is granted. For anything else (like PUT or DELETE), it makes sure the user making the request is the same as the post's author.

Finally, we just need to drop this new permission into our PostViewSet.

api/views.py

from .permissions import IsAuthorOrReadOnly

class PostViewSet(viewsets.ModelViewSet): # ... previous code ... permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]

By layering permissions like this, you can build complex, fine-grained access control rules with clean, reusable code. This is a far more scalable and secure approach than scattering if request.user == obj.author checks throughout your view logic.

Putting these security measures in place isn't an afterthought; it's a fundamental step. For a much deeper look into this topic, you might find it useful to read our guide on how to conduct a comprehensive software security audit to proactively find potential weak spots.

Common Questions You'll Run Into With DRF

As you get your hands dirty with this Django REST Framework tutorial, you're bound to have some questions pop up. It's totally normal. Here are some of the most common hurdles developers face, along with my take on how to clear them and keep your project moving.

When Do I Use an APIView vs. a ViewSet?

This is a classic DRF puzzle, and the answer really boils down to convention over configuration.

You'll want to reach for a standard APIView when you need total control over your endpoint's logic. It's the perfect tool for custom actions that don't neatly fit the typical Create, Read, Update, Delete (CRUD) pattern. Think of endpoints that process a payment, trigger a password reset email, or run a complex data report. An APIView essentially gives you a blank canvas to build whatever you need.

On the other hand, a ViewSet (especially a ModelViewSet) is your go-to when you're building a standard RESTful API for a Django model. It's a huge time-saver, providing all the CRUD actions right out of the box and integrating perfectly with Routers to slash boilerplate code. Honestly, for about 90% of your model-based resources, a ViewSet is the faster, cleaner, and more maintainable choice.

My rule of thumb is simple: If your endpoint maps directly to a model's CRUD operations, start with a ViewSet. If it does something unique or custom, use an APIView. This approach keeps your code predictable and easy for the next developer (or future you) to understand.

How Do I Add Custom Validation to a Serializer?

One of the best things about DRF serializers is that they're not just for converting data—they are powerful validation engines. You have a couple of solid ways to add your own rules.

  • For single-field validation: Just add a validate_<field_name> method to your serializer class. For instance, a validate_title method will only run against the title field. It's clean and specific.

  • For multi-field validation: When you need to compare multiple fields (like making sure an end_date is after a start_date), you'll want to override the validate() method. This gives you access to the entire object's data.

In either case, if the data doesn't pass your checks, you just raise a serializers.ValidationError. DRF handles the rest, automatically returning a helpful 400 Bad Request response with the error details.

Can I Use DRF with a NoSQL Database like MongoDB?

Yes, you definitely can, but it's not a native feature. DRF was built to work hand-in-glove with the Django ORM, which is all about SQL databases. To bridge that gap, you'll need a third-party library to do the heavy lifting.

For example, djongo is a popular package that lets your Django project talk to MongoDB. Once you have that connector set up, you'd typically use another tool like djangorestframework-mongoengine to create serializers and views that understand your NoSQL data structures. It takes a little extra setup, but it’s a common and totally viable solution for projects that need the flexibility of NoSQL.

What Is the Best Way to Version a DRF API?

Versioning is a non-negotiable if you want to maintain your API over the long term without breaking things for your users. DRF comes with several great ways to handle this.

The two I see most often are:

  1. URLPathVersioning: This is probably the most common and explicit method. You stick the version right in the URL, like /api/v1/posts/. It’s dead simple for developers to see and test in a browser.
  2. AcceptHeaderVersioning: This approach keeps your URLs cleaner. The client requests a specific version by sending an Accept HTTP header, like Accept: application/json; version=1.0.

Which one is "best"? It really depends on who will be using your API. Public-facing APIs often do well with the clarity of URL-based versioning.

Once your API is built and versioned, getting it live is the final hurdle. Learning about deployment in general is a huge asset. For a good overview, you can find resources on how to host your own website. This will give you the foundational knowledge to take your DRF project from your local machine to a real server.


At 42 Coffee Cups, we specialize in building high-performance web applications with Python/Django and Next.js. Whether you need to develop an MVP in under 90 days, augment your existing team, or modernize your architecture, our expert developers are ready to help. Discover how we can accelerate your growth at https://www.42coffeecups.com.

Share this article: