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.
Function based views (most code)
Class based views
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