Representing relationships with nested routes

A girl taking a selfie

In a previous post I explained how nested serializers could be used to represent relationships between Django models in your Django REST Framework API.

This works well when the number of related items is small, but what if you have related items numbering in the hundreds or thousands? For example, if we wanted to fetch Justin Bieber’s Twitter followers we probably wouldn’t want all 100 million Beliebers at once; rather, we’d probably fetch only a few of them at a time and then paginate through the rest.

Luckily, we can do this pretty easily using a few features built into Django REST Framework.

Prerequisites

If you want to follow along with this tutorial, install Django REST Framework and make sure you’ve added "rest_framework" to your INSTALLED_APPS list.

The setup

Let’s say that we’re building a pretend social network where users can follow each other. The models file could look something like this:

# socialnetwork/models.py

from django.db import models


class User(models.Model):
    username = models.SlugField(unique=True)
    followers = models.ManyToManyField("User")

    def __str__(self):
        return self.username

Note that I’m using SlugField for the username field—this makes sure the username can be used in a URL, which might come in handy later.

Given the model above, a first attempt at a serializer might look something like this:

# socialnetwork/serializers.py

from rest_framework.serializers import ModelSerializer
from .models import User


class UserSerializer(ModelSerializer):

    class Meta:
        model = User
        fields = ["username", "followers"]

This will simply output the user’s username and the primary keys of the user’s followers.

Let’s create a ModelViewSet for our users so we can try out our serializer:

# socialnetwork/views.py

from rest_framework.viewsets import ModelViewSet
from .models import User
from .serializers import UserSerializer


class UserViewSet(ModelViewSet):
    serializer_class = UserSerializer
    queryset = User.objects.all()
    lookup_field = "username"

In order to make the URLs a bit more readable I’ve set the lookup_field to "username"; this means users can be looked up by their username, e.g. instead of dull URLs like /users/123 we’ll get something with a bit more pizzazz, like /users/bieber

Finally, let’s wire the view up to a route. Open up the project’s urls.py and extend it with a router and some routes:

# project/urls.py

from django.contrib import admin
from django.urls import path
from rest_framework.routers import DefaultRouter

from socialnetwork.views import UserViewSet

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

urlpatterns = [
    path('admin/', admin.site.urls),
] + router.urls

To try it out, create a few users and let them follow each other. Now, if you run manage.py runserver and go to http://localhost:8000/users/ you should see something like this:

[
    {
        "username": "bieber",
        "followers": [ 2, 3 ]
    },
    {
        "username": "bieberfever",
        "followers": []
    },
    {
        "username": "true_belieber",
        "followers": []
    }
]

Nested routes

As I mentioned at the start of the post, including a list of each user’s followers in the response is problematic if the number of followers is large. Instead, what if we could have a separate URL just for listing followers, like /users/bieber/followers? This would also make the list easier to paginate, and we would only have to fetch followers when we actually needed them.

Well, I’m convinced—let’s create that followers view! We’ll start by creating a new ListAPIView where we override get_queryset to get the followers for a given user:

from rest_framework.generics import ListAPIView
from rest_framework.viewsets import ModelViewSet
from .models import User
from .serializers import UserSerializer


class UserViewSet(ModelViewSet):
    serializer_class = UserSerializer
    queryset = User.objects.all()
    lookup_field = "username"


class FollowersListView(ListAPIView):
    serializer_class = UserSerializer

    def get_queryset(self):
        username = self.kwargs["username"]
        user = get_object_or_404(User, username=username)
        return user.followers.all()

We’re assuming that we’ll receive the user’s username in a URL parameter called username, which means we’ll be able to fetch the username via self.kwargs["username"].

Note

In this example I’ve just used the same UserSerializer in both the “users” and “followers” views. However, in a “real” application you might want to use a more “lightweight” serializer for your list views; see my previous post for instructions on how to do this.

Let’s add a route for our “follwers” view, making sure to include the aforementioned username parameter:

from django.contrib import admin
from django.urls import path
from rest_framework.routers import DefaultRouter

from socialnetwork.views import UserViewSet, FollowersListView

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path(
        "users/<slug:username>/followers/",
        FollowersListView.as_view(),
        name="user-followers",
    )
] + router.urls

Now, try opening http://localhost:8000/users/bieber/followers; you should see a list of all of the users following bieber, i.e. something like this:

[
    {
        "username": "bieberfever",
        "followers": [],
    },
    {
        "username": "true_belieber",
        "followers": [],
    }
]

Neat! However, the followers key still contains a list of user IDs; let’s change it to a link that points to the “followers” URL for each user.

Next steps

Good job! We’ve now given each user a paginated list of followers that should be able to handle even the most popular of teen idols.

If you want to keep working on this example here are a few ideas that you could try out:

  • Add an endpoint where we can see all users that the given user is following, e.g. /users/<username>/following.
  • Add “follower count” and “following count” aggregate fields on UserSerializer so you can see at a glance how many followers a user has / how many users they’re following
  • As mentioned earlier, try using a different “lightweight” serializer for list views (as described in my post on nested serializers).

Wrapping up

That’s it for now! If you have any questions or comments I’d love to hear them—I’m on Twitter as @therealchreke if you want to get in touch. Also, don’t forget to subscribe to my email list if you want to be notified whenever I post something new on this blog!

See you later!

Want more posts like this, delivered straight to your inbox? Subscribe to my newsletter!