User Registration with Django REST Framework

User registration with Django REST Framework (DRF) can be challenging because you don't have the advantage of Django's built in user model. I am going to show you how to implement user registration with an API, without compromising user passwords.

Project Set Up

Start with a blank Django project. If you are unsure how, then please refer to my tutorial on how to install Django. This tutorial covers how to start a vanilla Django project. You will then need to install Django REST framework by running the following line.

pip install djangorestframework

Project Code

You can also start your project by cloning the START branch of the GitHub Repository. The main branch contains the finished code.

If you pull the repository, then don't run migrations straight away. We are going to create a custom user model, which needs to be done before migrations are run for the first time.

Tip

Django REST Framework has a steep learning curve if you're not already familiar with Django. I consider creating an API to be more advanced than creating a basic Django app. If you are struggling to understand the concepts in this tutorial then I recommend learning some basic Django first. I have a beginner-friendly tutorial series on creating a to-do list app, and I have tutorials on basic user registration and registering users with email.

1. Create Users App

Django projects are organised by apps. We will create an app to keep all code for managing users together.

If you cloned the repository, you can skip this step.

django-admin startapp users

2. Custom User Model

Django projects come with a user model by default. We can use this to create users with username and password without having to create a model ourselves. This is convenient until we need to make changes to the model.

Running migrations for the first time creates a table in the database for users. Once this has been done, you can't change the model class. Therefore, it is important to define a custom user model before running migrations. This gives us more control over the user model and we are free to customise it after migrations have been run.

# users/models.py

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

class User(AbstractUser):
    pass

AbstractUser vs AbstractBaseUser

The AbstractUser class provides more features which results in less code to write yourself. The AbstractBaseUser class only provides some basic authentication and will mean you have to write a lot of functionality yourself.

It is important that your model inherits from AbstractUser and not AbstractBaseUser.

You would only use AbstractBaseUser if you want complete control over the user model and are happy to do the extra work that comes with it.

It is possible to customise your User model using AbstractUser. For example, I have a tutorial that shows you how to switch the username field for an email field.

Tell Django where our User model is

We need to update our project settings define where our user model is located. If you skip this step, Django will continue to the use the default model.

Go to your project settings and define a constant called AUTH_USER_MODEL

# settings.py

AUTH_USER_MODEL = "users.User"

Create and run migrations

Now that Django knows where to find our custom user model, we can create and run migrations.

python manage.py makemigrations

Run migrations

python manage.py migrate

Migration files describe changes to models in terms of Python. Django uses the migrations to determine what SQL code needs to be applied to the database. Migrations ensure that the database and the models are kept in sync. I have a separate guide to understanding Django migrations if you get stuck.

3. Create a Serializer

Serializers are a core component of Django REST Framework APIs.

You can use serializers to:

  • Define which fields must be supplied in a request

  • Define the structure of an API response

  • Set fields as read-only or write-only

  • Define how objects are created in the database

First, create a file called serializers.py in the users app.

Django REST framework has a class called ModelSerializer, which we will use as a template.

Then create a class called UserSerializer, which inherits from ModelSerializer.

DRF's model serializer provides a lot of the functionality for parsing JSON requests and converting Python response objects back into JSON.

We need to point our serializer to our user model and tell it what fields to include.

Our user model doesn't have a field called 'url' but we should include it anyway. When requesting a list of users, the serializer will provide the URL to retrieve details of an individual user in the url field.

It is important that our password is write-only. If we skip this step, a user could see passwords (albeit encoded) in an API response.

# users/serializers.py

from rest_framework.serializers import ModelSerializer

from .models import User

class UserSerializer(ModelSerializer):

    class Meta:
        model = User
        fields = ["url", "username", "password"]
        extra_kwargs = {"password": {"write_only": True}}

Why do we need to define the create method?

Model serializers come with a method called create. It is a quick way to create objects in the database with data that the serializer has validated.

The method calls User.objects.create(...) with the username and password supplied by the user's API request.

The problem with this is this doesn't include anything to protect the password. Instead, Django will store the password as plain text in the database, leaving your API very vulnerable to attackers.

We need to encode the password before committing it to the database.

By overriding the create method. We can encode the password before saving it to the database.

To do this, we create an instance of the user class with just the username.

We then use the set_password method of our User model (inherited from AbstractUser).

The set_password method takes care of salting and hashing the password and returns the encoded value.

Only then can we save the user to the database.

Add the following code to your serializer.

class UserSerializer(ModelSerializer):

    class Meta:
        model = User
        fields = ["url", "username", "password"]
        extra_kwargs = {"password": {"write_only": True}}

    def create(self, validated_data):
        user = User(username=validated_data["username"])
        user.set_password(validated_data["password"])
        user.save()
        return user

4. Create a Viewset

There are 3 ways to create views for your Django API.

  1. Function based views (most code)

  2. Class based views

  3. Viewsets (least code)

Viewsets are perhaps the hardest to understand but involve the least code.

They are called Viewsets because they combine multiple views into a single class. They are the ultimate shortcut for developing an API with Django REST framework.

Viewsets handle the following types of requests in a single class. In brackets, I've put the HTTP method you would use to make the request.

  • Create (POST)

  • List (GET)

  • Retrieve (GET)

  • Partial Update (PATCH)

  • Update (PUT)

  • Destroy (DELETE)

By creating a viewset, you can create one class instead of six.

Create the UserViewset

DRF has a base class called ModelViewset, which we will use as a template. We will need to tell it what serializer class to use and provide a QuerySet for GET requests.

from rest_framework.viewsets import ModelViewSet

from .serializers import UserSerializer
from .permissions import UserPermission
from .models import User

class UserViewSet(ModelViewSet):

    serializer_class = UserSerializer
    queryset = User.objects.all().order_by("-date_joined")
    permission_classes = [UserPermission,]

You should get an error here because UserPermission is not defined. We will create it in the next step.

5. Create Permissions

Permissions are a critical part of APIs, especially when user data is involved.

If you don't specify permissions on your viewset, then anyone can read user data and anyone can delete users.

Django REST framework has Permissions classes you can use to define who is authorised to view a certain resource.

We also have to define the permission for each resource. Here's what we are going to do for users:

Resource

Who's Allowed

Create (POST)

Anyone

List (GET)

Administrators only

Retrieve (GET)

Administrator or owner

Update (PUT)

Administrator or owner

Partial Update (PATCH)

Administrator or owner

Delete (DELETE)

Administrators only

Django REST Framework has a number of built-in permissions, but if you need different permissions for each resource, then it is easier to write your own permission class.

To control permissions, we need to start by creating a file in our users app called permissions.py.

A permissions class contains two methods: has_permission and has_object_permission.

has_permission is called first. has_object_permission is only called if has_permission returns True.

The has_object_permission is only relevant for requests that are specific to one object (retrieve, update, partial update and destroy). It receives an additional argument called obj which we can use to check if an object belongs to the requesting user.

For create and list, the permission is determined by has_permission only.

For create requests, has_permission returns True without additional checks. This is because anyone can create a user.

For other requests, we can get the user from the request, and use their is_authenticated and is_staff statuses to determine whether they are allowed to proceed.

Credit to StackOverflow here for explaining how permissions work.

from rest_framework import permissions
from rest_framework.generics import GenericAPIView
from rest_framework.request import Request

from django.db import models

class UserPermission(permissions.BasePermission):
    """
    source: https://stackoverflow.com/questions/19313314/django-rest-framework-viewset-per-action-permissions
    """

    def has_permission(self, request: Request, view: GenericAPIView) -> bool:

        if view.action == "create":
            return True # anyone can create user, no additional checks needed.
        if view.action == "list":
            return request.user.is_authenticated and request.user.is_staff
        elif view.action in ["retrieve", "update", "partial_update", "destroy"]:
            return True  # defer to has_object_permission
        else:
            return False

    def has_object_permission(
        self, request: Request, view: GenericAPIView, obj: models.Model
    ) -> bool:

        if not request.user.is_authenticated:
            return False

        if view.action in ["retrieve", "update", "partial_update"]:
            return obj == request.user or request.user.is_staff
        elif view.action == "destroy":
            return request.user.is_staff
        else:
            return False

6. Create a Router

Our next step is to update urls.py so that the user's requests are routed to the correct view.

If you're using Viewsets, then you will need to add a router. If you are using traditional function-based views or class-based views, then you can continue to register your views in the urlpatterns list as normal.

Go to urls.py and add a router.

A router provides a shortcut for declaring URLs for ViewSets. Instead of declaring URLs for each type of request individually, the router will generate them for you.

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

from rest_framework import routers

from users.views import UserViewSet

router = routers.DefaultRouter()
router.register("users", UserViewSet)

urlpatterns = [
    path("api/", include(router.urls)),
    path("admin/", admin.site.urls),
    path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]

7. Testing our API

So far, we have created a custom user model, a serializer, a viewset, a router and declared URLs.

Our API is ready for testing.

The Browsable API

We did this so we can explore our API in the browser.

First, we need to start our server.

python manage.py runserver

Navigate to localhost:8000/api

You should get a screen like the one below. If that doesn't work, check that INSTALLED_APPS in settings.py includes 'rest_framework' and check rest_framework.urls has been included in urls.py (see previous section).

Responses include URLs to help navigate the browsable API.

However, we configured our permissions so that only admins can view the users list.

Unfortunately, the form to create users is hidden if the list view isn't visible.

To get around this, we need to log in as a superuser.

First, create a superuser by running the following line in your terminal and follow the prompts.

python manage.py createsuperuser

When your user has been successfully created, then you can log into the browser. You can find the Log In button at the top right of the screen.

Once you are logged in, you should see the user list and a form for creating new users.

How to make API requests with curl

We set up our permissions so that anyone can create users, not just superusers.

Another way to test the API is to use your terminal.

Go to your terminal and run the following:

curl -X POST -F "username=test" -F "password=test" http://localhost:8000/api/users/

This makes a POST request to http://localhost:8000/api/users. The -X flag precedes the request type and the -F flag precedes fields to be included in the request.

You should get an output like this.

{"url":"http://localhost:8000/api/users/4/","username":"test"}%

Conclusion

This tutorial covered how to register users with Django REST framework.

It involved:

  • Creating a custom user model

  • Creating a serializer (like a form but for APIs) to convert data between JSON and Python and validate requests

  • Created a ViewSet to handle requests to create, list, retrieve, update, partial update and delete users, all in one class

  • Created a custom permissions class to have full control over who can make each kind of request. We want anyone to be able to create a user, but only administrators can delete them.

  • Created a router to automatically generate URLs for our viewset

  • Registered URLs so that requests are routed to the correct view.

  • Made an API request using curl