Hello! Welcome back to our module on API Security and Best Practices.
In our last session, we took a high-level look at API versioning, discussing why it's crucial for managing change and describing the three common strategies: URL, header, and query parameter. We concluded that URL path versioning is often preferred for its clarity and ease of use.
Today, we move from theory to practice. Our goal is to implement URL-based versioning for a DRF API to support evolving requirements. You'll learn how Django REST Framework's built-in tools can be configured to manage different API versions cleanly and effectively, allowing your API to evolve without breaking existing clients.
A Quick Recap: Two URL-Based Strategies in DRF
As we touched on previously, DRF offers two main schemes for implementing URL-based versioning. From a client's perspective, they look identical—the version is in the URL path (e.g., /api/v1/resource/). The difference lies in how you configure it on the backend.
To understand these two approaches, let's turn to the official Django REST Framework documentation.
Please read these two sections from the DRF documentation on Versioning. They introduce the two main classes we will be working with for URL-based versioning.
Read the sections titled 'URLPathVersioning' and 'NamespaceVersioning'. Pay close attention to the urlpatterns examples in each to see how the server-side configuration differs.
To summarize the key difference:
URLPathVersioning: This scheme requires you to explicitly capture aversionkeyword argument in your URL pattern (e.g.,re_path(r'^(?P<version>(v1|v2))/...')). It's direct but can make your URL patterns more complex if you have many routes.NamespaceVersioning: This scheme determines the version from the URL's namespace. You create different URL prefixes (/v1/,/v2/) and include your app's URLs under different namespaces. This is often considered cleaner and more manageable for larger projects.
For the remainder of this lesson, we will focus on implementing NamespaceVersioning due to its superior organization and scalability.
Implementing NamespaceVersioning: A Step-by-Step Guide
Let's imagine a practical scenario. We have a Product API.
Version 1 (v1) of our API returns product data with a simple name field and a stock_count field.
Version 2 (v2) introduces a breaking change. The business decides to rename stock_count to inventory_level for clarity and add a is_active boolean field.
- v1 Response:
{"name": "Laptop Pro", "stock_count": 150} - v2 Response:
{"name": "Laptop Pro", "inventory_level": 150, "is_active": true}
Our goal is to serve both versions simultaneously from URLs /api/v1/products/{id}/ and /api/v2/products/{id}/.
Step 1: Configure DRF for Versioning
First, we need to tell DRF which versioning scheme to use. In your project's settings.py, add the DEFAULT_VERSIONING_CLASS.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
# ... other settings
}
This single line instructs DRF to inspect the URL namespace of incoming requests to determine the version.
Step 2: Create Version-Specific Serializers
Since v1 and v2 have different data structures, we need two different serializers.
# products/serializers.py
from rest_framework import serializers
from .models import Product
class ProductSerializerV1(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['name', 'stock_count']
class ProductSerializerV2(serializers.ModelSerializer):
# We use source='stock_count' to map the model field to a different serializer field
inventory_level = serializers.IntegerField(source='stock_count')
class Meta:
model = Product
fields = ['name', 'inventory_level', 'is_active']
Step 3: Create a Version-Aware View
Instead of creating two separate views, we can create a single, "version-aware" view that selects the correct serializer based on the requested version. This is a very clean and common pattern. The request.version attribute, populated by our versioning scheme, is the key.
# products/views.py
from rest_framework import generics
from .models import Product
from .serializers import ProductSerializerV1, ProductSerializerV2
class ProductDetailView(generics.RetrieveAPIView):
queryset = Product.objects.all()
def get_serializer_class(self):
# The 'version' is determined by the URL namespace
if self.request.version == 'v1':
return ProductSerializerV1
# Default to v2 if not v1 or if version is not specified
return ProductSerializerV2
By overriding get_serializer_class, we can dynamically return the correct serializer class based on the version DRF detected from the URL namespace.
Step 4: Configure URL Namespacing
This is where NamespaceVersioning comes to life. First, define the URLs for your app's endpoints as you normally would.
# products/urls.py
from django.urls import path
from .views import ProductDetailView
# NO versioning is mentioned here. These are the reusable URL patterns.
urlpatterns = [
path('products/<int:pk>/', ProductDetailView.as_view(), name='product-detail'),
]
Now, in your project's root urls.py, you include this set of patterns twice, each under a different path and a different namespace.
# project/urls.py
from django.urls import path, include
urlpatterns = [
# Requests to /api/v1/... will be routed with the namespace 'v1'
path('api/v1/', include('products.urls', namespace='v1')),
# Requests to /api/v2/... will be routed with the namespace 'v2'
path('api/v2/', include('products.urls', namespace='v2')),
# ... other project urls like admin
]
This is the magic of NamespaceVersioning. When a request comes in for /api/v1/products/1/, Django's URL resolver matches the v1 namespace. DRF then sets request.version = 'v1', which our ProductDetailView uses to select ProductSerializerV1. The same logic applies to /api/v2/.
This approach is highly organized. If you want to add a v3, you don't have to touch the products app at all. You just add a new ProductSerializerV3, update the logic in the view, and add one line to the root urls.py.
The following resource provides a similar, complete example. While it uses separate views for each version (another valid approach), notice the core similarity in the urls.py setup, which is the key to URL-based versioning.
REST API Versioning in Django: Best Practices for ...
This article provides a concise, full example of implementing URL versioning. Focus on the urls.py section to reinforce what we just discussed.
Read the sections 'Code Example: Implementing URL Versioning in Django' and 'Update urls.py'. This demonstrates how distinct URL paths are created for v1 and v2, which is the fundamental mechanism for this strategy.
Test your understanding!
Your Product API, versioned with NamespaceVersioning for v1 and v2, is a success. Now, a new requirement has come in for a v3. In this new version, the name field must be renamed to product_title.
Describe the three specific code changes you would need to make to add this v3 endpoint while leaving v1 and v2 completely unaffected.
Show answer
Here are the three necessary steps:
-
Create a new serializer (
products/serializers.py): You would define aProductSerializerV3that maps the model'snamefield to the newproduct_titlefield.class ProductSerializerV3(serializers.ModelSerializer): product_title = serializers.CharField(source='name') class Meta: model = Product fields = ['product_title', 'inventory_level', 'is_active'] -
Update the view logic (
products/views.py): You would add a condition to theget_serializer_classmethod to handle the new'v3'version.def get_serializer_class(self): if self.request.version == 'v1': return ProductSerializerV1 if self.request.version == 'v3': # Add this condition return ProductSerializerV3 return ProductSerializerV2 -
Add the new URL namespace (
project/urls.py): You would add a new path to your rooturls.pyto expose thev3endpoints.urlpatterns = [ path('api/v1/', include('products.urls', namespace='v1')), path('api/v2/', include('products.urls', namespace='v2')), path('api/v3/', include('products.urls', namespace='v3')), # Add this line ]
Conclusion
Congratulations! You've successfully moved from understanding API versioning concepts to implementing a robust, scalable versioning strategy in a real-world Django project. This skill is essential for any backend developer working on APIs that need to evolve over time.
Key Takeaways:
- API versioning in DRF is enabled via the
DEFAULT_VERSIONING_CLASSsetting. - URL-based versioning is a clear and popular strategy where the version is part of the URL path (e.g.,
/api/v1/). NamespaceVersioningis a highly organized way to implement this, using URL namespaces to separate versions.- The
request.versionattribute becomes available in your views, allowing you to create version-aware logic, typically by overridingget_serializer_class()to select the appropriate serializer for the requested version.
Preview of the Next Lesson:
We've spent the last few lessons securing different aspects of our API's logic and structure. Now, we'll shift our focus to securing the data itself as it travels between the client and the server. In our next lesson, we will explore the role of TLS/SSL in providing encryption and integrity for data in transit, which is the technology that powers HTTPS and is fundamental to all modern web security.