Django from Basics to Production · APIs with Django REST Framework, and Production

ViewSets, Routers, Permissions and Pagination

Expose a model as a REST API with a ViewSet and router, restrict access per user, and paginate results.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A /notes/ API with a ModelViewSet and router: authenticated users see and edit only their own notes (filtered queryset plus an ownership permission), the owner is set from the request user, and lists are paginated.

The idea

  • ViewSets bundle list, retrieve, create, update and delete; a router generates the URLs.
  • Permissions (IsAuthenticated, custom has_object_permission) decide access; the queryset decides what's visible. Filter the queryset by user so other users' objects are a 404, not a 403.
  • Set server-owned fields in perform_create(serializer.save(owner=...)).
  • Pagination (page number, limit/offset, cursor) keeps responses bounded.

The code

A per-user notes API
import sys, types
import django
from django.conf import settings

sys.modules["shop"] = types.ModuleType("shop")       # lets this single file act as the "shop" app
sys.modules["shop"].__file__ = __file__
settings.configure(
    SECRET_KEY="dev-only", DEBUG=False, ALLOWED_HOSTS=["testserver"], ROOT_URLCONF=__name__,
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
    INSTALLED_APPS=["django.contrib.contenttypes", "django.contrib.auth", "rest_framework", "shop"],
)
django.setup()

from django.contrib.auth.models import User
from django.core.management import call_command
from django.db import connection, models
from rest_framework import pagination, permissions, routers, serializers, viewsets

call_command("migrate", verbosity=0)

class Note(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    text = models.CharField(max_length=200)
    class Meta: app_label = "shop"; ordering = ["id"]

with connection.schema_editor() as editor:
    editor.create_model(Note)

class NoteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Note
        fields = ["id", "text"]

class SmallPages(pagination.PageNumberPagination):
    page_size = 2

class NoteViewSet(viewsets.ModelViewSet):
    serializer_class = NoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    pagination_class = SmallPages

    def get_queryset(self):
        return Note.objects.filter(owner=self.request.user)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

router = routers.DefaultRouter()
router.register("notes", NoteViewSet, basename="note")
urlpatterns = router.urls

from rest_framework.test import APIClient
ana, ben = User.objects.create_user("ana"), User.objects.create_user("ben")
ben_note = Note.objects.create(owner=ben, text="ben's secret")
client = APIClient()
print("anonymous:", client.get("/notes/").status_code)
client.force_authenticate(ana)
for text in ["buy milk", "call mum", "book dentist"]:
    client.post("/notes/", {"text": text}, format="json")
page1 = client.get("/notes/").json()
print("page 1:", page1["count"], "notes,", [n["text"] for n in page1["results"]], "next:", page1["next"])
print("ben's note as ana:", client.get(f"/notes/{ben_note.pk}/").status_code)

Output

anonymous: 403
page 1: 3 notes, ['buy milk', 'call mum'] next: http://testserver/notes/?page=2
ben's note as ana: 404

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • queryset = Note.objects.all() with only a permission check on list: users see everyone's notes.
  • Accepting owner from the request body instead of setting it from request.user.
  • No default pagination, so list endpoints return everything as data grows.

Key points

  • ViewSets plus routers give you a full REST resource.
  • Scope the queryset to the user; set owners server-side.
  • Configure permissions and pagination as defaults in REST_FRAMEWORK settings.

Further reading: ViewSets · Permissions · Pagination

Practice questions

Why filter get_queryset by user rather than relying only on object permissions?

Answer

List endpoints don't check object permissions per row, so an unfiltered queryset leaks other users' data; filtering also makes others' objects a 404.

How should a viewset set the owner of a created object?

Answer

In perform_create by calling serializer.save(owner=self.request.user), never from client input.

Why configure pagination by default?

Answer

Without it list endpoints return every row, which grows slower and heavier as data increases.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?