python
58 lines · 8 steps
How URL-namespaced API versioning works in Django
Two parallel URL groups route the same product endpoints to version-specific views and serializers so v1 and v2 can evolve independently.
Explained by
highlit
1from django.urls import path, include
2
3app_name = "api"
4
5v1_patterns = [
6 path("products/", ProductListV1.as_view(), name="product-list"),
7 path("products/<int:pk>/", ProductDetailV1.as_view(), name="product-detail"),
8]
9
10v2_patterns = [
11 path("products/", ProductListV2.as_view(), name="product-list"),
12 path("products/<int:pk>/", ProductDetailV2.as_view(), name="product-detail"),
13]
14
15urlpatterns = [
16 path("v1/", include((v1_patterns, "v1"), namespace="v1")),
17 path("v2/", include((v2_patterns, "v2"), namespace="v2")),
18]
19
20
21class ProductSerializerV1(serializers.ModelSerializer):
22 class Meta:
23 model = Product
24 fields = ["id", "name", "price"]
25
26
27class ProductSerializerV2(serializers.ModelSerializer):
28 price_cents = serializers.IntegerField(source="price_in_cents", read_only=True)
29 currency = serializers.CharField(read_only=True)
30
31 class Meta:
32 model = Product
33 fields = ["id", "name", "price_cents", "currency", "created_at"]
34
35
36class ProductListV1(generics.ListCreateAPIView):
37 queryset = Product.objects.active()
38 serializer_class = ProductSerializerV1
39
40
41class ProductDetailV1(generics.RetrieveUpdateDestroyAPIView):
42 queryset = Product.objects.active()
43 serializer_class = ProductSerializerV1
44
45
46class ProductListV2(generics.ListCreateAPIView):
47 queryset = Product.objects.active().select_related("category")
48 serializer_class = ProductSerializerV2
49
50
51class ProductDetailV2(generics.RetrieveUpdateDestroyAPIView):
52 queryset = Product.objects.active().select_related("category")
53 serializer_class = ProductSerializerV2
54
55 def get_serializer_context(self):
56 context = super().get_serializer_context()
57 context["request_version"] = "v2"
58 return context
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Isolating each API version behind its own URL prefix and namespace lets you change payloads without breaking existing clients.
- 2Swapping serializer_class per view is how the same model yields different response shapes across versions.
- 3Overriding get_serializer_context passes request-scoped data like the version down into serializer logic.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
python
import re from functools import total_ordering from typing import Optional
Parsing and comparing semantic versions
regex
operator-overloading
sorting
Intermediate
7 steps
python
from typing import Any, Sequence, Mapping def render_markdown_table(
Rendering an aligned Markdown table in Python
string formatting
data transformation
closures
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-url-namespaced-api-versioning-works-in-django-explained-python-86a6/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.