تا الان چندین API ساختید. فیلتر و جستجو و صفحهبندی دارید. احراز هویت و مجوزها را هم اضافه کردید. اما یک سؤال مهم هنوز بیجواب مانده است. اگر بخواهید یک فیلد را تغییر دهید، چه اتفاقی میافتد؟ مثلاً فعلاً مدل Product شما فیلد name دارد. یک ماه بعد تصمیم میگیرید اسم آن را بگذارید title. برای پروژه جدیدتان عالی است.
اما اپلیکیشن موبایل کاربران قدیمی هنوز name را میفرستد. وبسایتهای دیگری که از API شما استفاده میکنند، همان name را انتظار دارند. اگر مستقیم تغییر دهید، همه چیز برایشان خراب میشود. این مشکل را نسخهبندی API حل میکند. نسخهبندی یعنی چند نسخه از API را همزمان نگه دارید. کاربران قدیمی به نسخه قدیمی متصل بمانند. کاربران جدید از نسخه جدید استفاده کنند. هر کسی بدون مشکل به کار خود ادامه دهد.
در این درس، دو روش اصلی نسخهبندی را یاد میگیرید.
روش اول: URLPathVersioning. نسخه را در خود آدرس قرار میدهید. مثل /api/v1/products/ و /api/v2/products/. ساده و شفاف.
روش دوم: AcceptHeaderVersioning. نسخه را در هدر درخواست مشخص میکنید. برای APIهایی که نمیخواهند آدرسشان شلوغ شود، مناسب است.
همچنین یاد میگیرید چطور نسخهبندی را در DRF فعال کنید و در ویوها به نسخه درخواست دسترسی داشته باشید.
این درس برای وقتی است که API شما رشد میکند و چندین مشتری به آن متصل هستند. نسخهبندی به شما اجازه میدهد بدون ترس از خراب کردن کار دیگران، API خود را بهبود ببخشید.
چرا نسخهبندی لازم است؟
فرض کنید یک API فروشگاه ساختهاید. مشتریها از آن استفاده میکنند. یک اپلیکیشن موبایل به آن متصل است. یک سایت خبری هم از آن محصولات را نمایش میدهد. حالا بعد از چند ماه، تصمیم میگیرید یک فیلد جدید به خروجی اضافه کنید. یا نام یک فیلد را تغییر دهید. یا شاید منطق یک عملیات را عوض کنید.
اگر مستقیم این تغییرات را روی همان API اعمال کنید، چه اتفاقی میافتد؟ اپلیکیشن موبایل کاربران از کار میافتد. چون فیلدی که انتظار داشت، دیگر وجود ندارد. سایت خبری دیگر محصولات را درست نشان نمیدهد. همه چیز به هم میریزد. راه حل چیست؟ نسخهبندی.
یعنی به جای این که یک API داشته باشید، چند نسخه از آن را نگه میدارید. نسخه اول برای کاربران قدیمی. نسخه دوم برای کاربران جدید. کاربران قدیمی همچنان به نسخه اول متصل هستند. هیچ تغییری احساس نمیکنند. کاربران جدید از نسخه دوم استفاده میکنند. امکانات جدید را دارند. وقتی همه کاربران به نسخه دوم مهاجرت کردند، نسخه اول را retire (بازنشسته) میکنید. اما تا آن روز، هر دو نسخه کنار هم کار میکنند.
سناریوی واقعی
فیسبوک، گوگل، توییتر، همه APIهای خود را نسخهبندی میکنند. وقتی فیسبوک قابلیت جدیدی اضافه میکند، یک نسخه جدید میدهد. توسعهدهندگانی که اپلیکیشنهای قدیمی دارند، همچنان میتوانند از نسخه قدیمی استفاده کنند. برای مهاجرت به نسخه جدید، زمان دارند. بدون نسخهبندی، هر تغییر کوچکی باعث میشود هزاران برنامه از کار بیفتند.
بدون نسخهبندی چه خطری داریم؟
سه خطر اصلی وجود دارد.
- اول: نمیتوانید اشتباهات طراحی اولیه را اصلاح کنید. چون هر تغییری یعنی خراب شدن کار دیگران.
- دوم: اضافه کردن قابلیت جدید سخت میشود. چون نمیدانید کاربران قدیمی با آن چه رفتاری دارند.
- سوم: مستندات API همیشه دقیق نیست. چون مدام تغییر میکند.
نسخهبندی در DRF
خوشبختانه DRF نسخهبندی را از قبل پشتیبانی میکند. میتوانید مشخص کنید هر ویو یا هر ViewSet برای کدام نسخه است. DRF خودکار مسیرها را مدیریت میکند و نسخه درخواست را به ویو شما میرساند.
دو روش اصلی وجود دارد:
- گذاشتن نسخه در آدرس (مثل /api/v1/products/)
- گذاشتن نسخه در هدر درخواست (مثل Accept: application/json; version=1.0)
هر دو را در بخش های بعدی این درس یاد میگیرید.
یک مثال ساده
فرض کنید نسخه اول API شما فیلد name دارد. نسخه دوم فیلد full_name دارد.
# نسخه ۱
class ProductSerializerV1(serializers.Serializer):
name = serializers.CharField()
# نسخه ۲
class ProductSerializerV2(serializers.Serializer):
full_name = serializers.CharField()
هر دو نسخه همزمان کار میکنند. کاربر قدیمی که name را میخواند، همان را میگیرد. کاربر جدید full_name را میگیرد. هیچکدام از وجود دیگری خبر ندارد.
خلاصه این بخش
نسخهبندی یعنی به مشتریهای خود احترام بگذارید. به آنها فرصت بدهید خودشان را با تغییرات شما وفق بدهند. بدون اینکه یکدفه کارشان قفل شود. در دنیای واقعی، هیچ API جدی بدون نسخهبندی کار نمیکند. اگر میخواهید API شما حرفهای باشد و مشتری واقعی داشته باشد، نسخهبندی را از روز اول در نظر بگیرید.
روش URLPathVersioning
این روش سادهترین و رایجترین روش نسخهبندی است. در این روش نسخه API را مستقیماً در آدرس قرار میدهید.
https://api.example.com/v1/products/
https://api.example.com/v2/products/
https://api.example.com/v3/products/
کاربر و توسعهدهنده با یک نگاه میفهمد دارد از کدام نسخه استفاده میکند. خطایابی راحتتر است. تست دستی در مرورگر هم ساده میشود.
قدم اول: تنظیم در settings.py
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2', 'v3'],
'VERSION_PARAM': 'version',
}
توضیح پارامترها:
- DEFAULT_VERSIONING_CLASS: روش نسخهبندی را مشخص میکند
- DEFAULT_VERSION: اگر کاربر نسخهای نفرستاد، از این استفاده کن
- ALLOWED_VERSIONS: چه نسخههایی مجاز هستند
- VERSION_PARAM: نام پارامتر نسخه در آدرس (پیشفرض version است)
قدم دوم: تنظیم مسیرها (urls.py)
در فایل urls.py اصلی پروژه، باید پارامتر نسخه را به مسیرها اضافه کنید:
# urls.py اصلی
from django.urls import path, include
urlpatterns = [
path('api/<str:version>/', include('myapp.urls')),
]
حالا در فایل urls.py اپ خود، دیگر خبری از نسخه نیست:
# myapp/urls.py
from django.urls import path
from .views import ProductListView
urlpatterns = [
path('products/', ProductListView.as_view(), name='product-list'),
]
نتیجه: آدرس نهایی به این شکل میشود:
GET /api/v1/products/
GET /api/v2/products/
قدم سوم: استفاده از نسخه در ویو
در ویوها میتوانید به نسخه درخواست دسترسی داشته باشید و بر اساس آن رفتار متفاوتی نشان دهید:
# views.py
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from .models import Product
from .serializers import ProductSerializerV1, ProductSerializerV2
class ProductListView(ListAPIView):
queryset = Product.objects.all()
def get_serializer_class(self):
version = self.request.version
if version == 'v1':
return ProductSerializerV1
elif version == 'v2':
return ProductSerializerV2
return ProductSerializerV1 # پیشفرض
def get(self, request, *args, **kwargs):
serializer_class = self.get_serializer_class()
serializer = serializer_class(self.get_queryset(), many=True)
return Response({
'version': request.version,
'data': serializer.data
})
مثال: دو نسخه متفاوت
سریالایزر نسخه اول:
# serializers.py
class ProductSerializerV1(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
price = serializers.IntegerField()
سریالایزر نسخه دوم (با فیلدهای جدید):
class ProductSerializerV2(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField() # name به title تغییر کرده
price = serializers.IntegerField()
discount = serializers.IntegerField() # فیلد جدید
final_price = serializers.SerializerMethodField()
def get_final_price(self, obj):
return obj.price - (obj.price * obj.discount // 100)
تست در مرورگر
حالا میتوانید هر دو نسخه را تست کنید:
# نسخه اول
GET /api/v1/products/
# نسخه دوم
GET /api/v2/products/
هر کدام خروجی متفاوتی برمیگرداند. کاربران قدیمی به نسخه اول متصل هستند و تغییری نمیبینند. کاربران جدید از نسخه دوم استفاده میکنند.
مزایای URLPathVersioning
- سادگی: همه چیز در آدرس مشخص است
- قابل کش کردن: مرورگرها و CDNها میتوانند هر نسخه را جداگانه کش کنند
- تست آسان: میتوانید با تغییر عدد در آدرس، نسخهها را امتحان کنید
- مستندسازی شفاف: مستندات میتواند بگوید «برای استفاده از نسخه ۲، از آدرس v2 استفاده کنید»
معایب URLPathVersioning
- شلوغی آدرسها: همه آدرسها یک v1 یا v2 اضافی دارند
- تغییر در urls.py: باید مسیرها را طوری تنظیم کنید که پارامتر نسخه را قبول کنند
- تغییر در کلاینت: کاربر باید کد خود را تغییر دهد تا به آدرس جدید برود
چه موقع از این روش استفاده کنیم؟
| سناریو | توصیه |
| API عمومی با چند مشتری خارجی | عالی است |
| نیاز به کش کردن در CDN | عالی است |
| مستندات عمومی دارید | عالی است |
| کاربران شما توسعهدهندههای مستقلی هستند | عالی است |
خطاهای رایج
خطا: No version specified
یعنی کاربر نسخهای در آدرس نفرستاده. مثلاً رفته api/products/ به جای api/v1/products/.
راه حل: حتماً در مستندات بنویسید که نسخه اجباری است. یا DEFAULT_VERSION را در تنظیمات بگذارید.
خطا: Version not allowed
یعنی کاربر نسخهای فرستاده که در ALLOWED_VERSIONS نیست. مثلاً v5 در حالی که فقط v1 تا v3 دارید.
تمرین
یک پروژه جدید با دو نسخه API بسازید:
- نسخه v1: فقط فیلدهای id، name و price را برگرداند
- نسخه v2: فیلدهای id، title، price، stock و created_at را برگرداند
- هر دو نسخه را با URLPathVersioning پیادهسازی کنید و در Postman تست کنید.
روش AcceptHeaderVersioning
در روش قبلی، نسخه را در آدرس میگذاشتید. اینجا اما نسخه در هدر درخواست فرستاده میشود.
آدرس تمیز میماند. بدون v1 و v2 اضافی.
GET /api/products/
اما در هدر Accept، کلاینت مشخص میکند کدام نسخه را میخواهد:
Accept: application/json; version=1.0
یا
Accept: application/json; version=2.0
قدم اول: تنظیم در settings.py
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'DEFAULT_VERSION': '1.0',
'ALLOWED_VERSIONS': ['1.0', '2.0', '3.0'],
'VERSION_PARAM': 'version',
}
توضیح پارامترها:
- DEFAULT_VERSIONING_CLASS: روش نسخهبندی
- DEFAULT_VERSION: اگر کاربر نسخهای نفرستاد، از این استفاده کن
- ALLOWED_VERSIONS: چه نسخههایی مجاز هستند
- VERSION_PARAM: نام پارامتر در هدر Accept (پیشفرض version است)
قدم دوم: تنظیم مسیرها (urls.py)
در این روش، مسیرها کاملاً معمولی هستند. خبری از <str:version> در آدرس نیست.
# urls.py اصلی
from django.urls import path, include
urlpatterns = [
path('api/', include('myapp.urls')),
]
# myapp/urls.py
from django.urls import path
from .views import ProductListView
urlpatterns = [
path('products/', ProductListView.as_view(), name='product-list'),
]
آدرس نهایی
GET /api/products/
هیچ v1 یا v2 در آدرس نیست. همه چیز تمیز است.
قدم سوم: استفاده از نسخه در ویو
دقیقاً مثل روش قبلی. فقط self.request.version را میخوانید:
# views.py
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from .models import Product
from .serializers import ProductSerializerV1, ProductSerializerV2
class ProductListView(ListAPIView):
queryset = Product.objects.all()
def get_serializer_class(self):
version = self.request.version
if version == '2.0':
return ProductSerializerV2
return ProductSerializerV1 # پیشفرض
def get(self, request, *args, **kwargs):
serializer_class = self.get_serializer_class()
serializer = serializer_class(self.get_queryset(), many=True)
return Response({
'version': request.version,
'data': serializer.data
})
تست در Postman
در Postman، به برگه Headers بروید. یک هدر جدید اضافه کنید:
| Key | Value |
| Accept | application/json; version=1.0 |
درخواست بفرستید GET /api/products/. نسخه اول را میگیرید.
حالا هدر را به application/json; version=2.0 تغییر دهید. دوباره درخواست بفرستید. نسخه دوم را میگیرید.
تست در خط فرمان (curl)
# نسخه 1.0
curl -H "Accept: application/json; version=1.0" http://localhost:8000/api/products/
# نسخه 2.0
curl -H "Accept: application/json; version=2.0" http://localhost:8000/api/products/
مزایای AcceptHeaderVersioning
- آدرس تمیز: خبری از v1 و v2 در آدرس نیست
- تغییر آدرس نمیدهد: کلاینت فقط هدر را عوض میکند، آدرس یکسان میماند
- مناسب برای APIهای داخلی: وقتی کنترل کلاینتها را دارید
- عدم تغییر در مستندات آدرس: مستندات آدرس ثابت میماند
معایب AcceptHeaderVersioning
- تست در مرورگر سخت است: مرورگرها به راحتی نمیتوانند هدر سفارشی بفرستند
- پیچیدگی بیشتر برای کلاینت: توسعهدهنده باید هدر را درست تنظیم کند
- کش کردن سختتر: CDNها معمولاً بر اساس آدرس کش میکنند، نه هدر
- پیدا کردن نسخه در نگاه اول: از روی آدرس نمیفهمید کدام نسخه استفاده میشود
مقایسه دو روش
| ویژگی | URLPathVersioning | AcceptHeaderVersioning |
| نسخه در آدرس | بله | خیر |
| تست در مرورگر | آسان | سخت |
| کش کردن در CDN | عالی | محدود |
| تمیزی آدرس | کمتر | بیشتر |
| مناسب برای API عمومی | بله | محدود |
| مناسب برای API داخلی | بله | عالی |
مثال: دو نسخه با تغییرات بزرگ
فرض کنید در نسخه دوم، یک عملیات جدید به نام bulk_create اضافه کردهاید:
class ProductViewSet(ModelViewSet):
queryset = Product.objects.all()
def get_serializer_class(self):
if self.request.version == '2.0':
return ProductSerializerV2
return ProductSerializerV1
@action(detail=False, methods=['post'])
def bulk_create(self, request):
# فقط در نسخه ۲.۰ وجود دارد
if request.version != '2.0':
return Response({'error': 'این عملیات در این نسخه پشتیبانی نمیشود'}, status=404)
# منطق ساخت دستهجمعی
return Response({'status': 'created'})
عیبیابی خطاهای رایج
خطا: Accept header parsing error
یعنی فرمت هدر اشتباه است. درست: application/json; version=1.0 (با فاصله بعد از ;)
خطا: Version not allowed
یعنی نسخهای که کاربر فرستاده در ALLOWED_VERSIONS نیست.
نکته: بعضی کلاینتها ممکن است Accept هدر را درست تنظیم نکنند. حتماً در مستندات خود مثال دقیق بزنید.
چه موقع از AcceptHeaderVersioning استفاده کنیم؟
| سناریو | توصیه |
| API داخلی شرکت (مصرفکننده را کنترل دارید) | عالی است |
| API موبایل (نسخه اپ را میدانید) | خوب است |
| API عمومی با مشتریان ناشناس | توصیه نمیشود |
| نیاز به کش کردن در CDN | توصیه نمیشود |
تمرین این بخش
یک API ساده با دو نسخه بسازید:
- نسخه 1.0: فیلدهای id، name، price
- نسخه 2.0: فیلدهای id، title، price، discount
- از AcceptHeaderVersioning استفاده کنید. در Postman هر دو نسخه را تست کنید.
همچنین سعی کنید در مرورگر عادی (بدون Postman) آدرس را باز کنید. ببینید چه نسخهای برمیگرداند (به DEFAULT_VERSION نگاه کنید).
روش QueryParameterVersioning
در این روش، نسخه را به عنوان یک پارامتر در آدرس قرار میدهید. همانطور که پارامترهای ?search=django یا ?page=2 را اضافه میکنید، حالا ?version=v1 هم اضافه میکنید.
آدرس به شکل زیر میشود:
GET /api/products/?version=v1
GET /api/products/?version=v2
آدرس پایه یکی است. فقط پارامتر version عوض میشود.
قدم اول: تنظیم در settings.py
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.QueryParameterVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2', 'v3'],
'VERSION_PARAM': 'version', # نام پارامتر در آدرس
}
اگر VERSION_PARAM را تغییر بدهید، مثلاً به 'ver'، کاربر باید بنویسد ?ver=v1.
قدم دوم: تنظیم مسیرها (urls.py)
در این روش، مسیرها کاملاً معمولی هستند. مثل روزهای اول بدون نسخهبندی.
# urls.py اصلی
from django.urls import path, include
urlpatterns = [
path('api/', include('myapp.urls')),
]
# myapp/urls.py
from django.urls import path
from .views import ProductListView
urlpatterns = [
path('products/', ProductListView.as_view(), name='product-list'),
]
آدرس نهایی:
GET /api/products/?version=v1
هیچ تغییری در urls.py نمیدهید. فقط کلاینت پارامتر را به آدرس اضافه میکند.
قدم سوم: استفاده از نسخه در ویو
دقیقاً مثل دو روش قبلی. نسخه در request.version در دسترس است:
# views.py
from rest_framework.generics import ListAPIView
from .models import Product
from .serializers import ProductSerializerV1, ProductSerializerV2
class ProductListView(ListAPIView):
queryset = Product.objects.all()
def get_serializer_class(self):
version = self.request.version
if version == 'v2':
return ProductSerializerV2
return ProductSerializerV1
تست در مرورگر
سادهترین روش برای تست. فقط کافی است آدرس را در مرورگر باز کنید:
http://127.0.0.1:8000/api/products/?version=v1
http://127.0.0.1:8000/api/products/?version=v2
نیازی به Postman نیست. نیازی به هدر خاصی نیست. فقط کافی است پارامتر را به آدرس اضافه کنید.
| ویژگی | URLPathVersioning | AcceptHeaderVersioning | QueryParameterVersioning |
| نسخه کجاست؟ | در مسیر (/v1/) | در هدر (Accept) | در پارامتر (?version=v1) |
| تست در مرورگر | آسان | سخت | خیلی آسان |
| آدرس تمیز | نه (/v1/ اضافه دارد) | بله | نسبتاً (پارامتر اضافه دارد) |
| کش کردن در CDN | عالی | محدود | خوب (با احتیاط) |
| مستندسازی | شفاف | کمی پیچیده | شفاف |
مزایای QueryParameterVersioning
مزیت اول: سادهترین روش برای تست
برای تست کافی است آدرس را در مرورگر باز کنید و پارامتر را عوض کنید. بدون Postman، بدون تنظیم هدر.
مزیت دوم: تغییر آسان برای کلاینت
توسعهدهنده فقط کافی است یک پارامتر به آدرس اضافه کند. نیازی به تغییر ساختار آدرس یا تنظیم هدر نیست.
مزیت سوم: عدم تغییر در urls.py
بر خلاف روش URLPathVersioning، نیازی نیست مسیرها را تغییر دهید. کد urls.py تمیز میماند.
معایب QueryParameterVersioning
عیب اول: شلوغی آدرس
اگر فیلتر و جستجو و صفحهبندی هم داشته باشید، آدرس خیلی شلوغ میشود:
GET /api/products/?version=v2&search=laptop&page=3&ordering=-price
عیب دوم: رفتار CDNها
خیلی از CDNها بر اساس آدرس کامل کش میکنند. اما بعضی از آنها پارامترهای کوئری را نادیده میگیرند. ممکن است نسخههای مختلف با هم قاطی شوند.
عیب سوم: اجباری نبودن پارامتر
کاربر میتواند فراموش کند version را بفرستد. در این صورت، DEFAULT_VERSION استفاده میشود. اگر کاربر عمداً نسخه اشتباه بفرستد، ALLOWED_VERSIONS جلوی آن را میگیرد.
چه موقع از این روش استفاده کنیم؟
| سناریو | توصیه |
| API داخلی شرکت (کنترل دارید) | عالی است |
| مرحله توسعه و تست | عالی است |
| مستندات تعاملی (مثل Swagger) | خوب است |
| API عمومی با مشتریان ناشناس | قابل قبول |
| نیاز به کش کردن پیشرفته در CDN | توصیه نمیشود |
یک نکته مهم درباره ترتیب
در تنظیمات settings.py، ترتیب DEFAULT_VERSIONING_CLASS را عوض نکنید. فقط یکی از این روشها میتواند فعال باشد. نمیتوانید همزمان از دو روش استفاده کنید.
عیبیابی خطاهای رایج
خطا: پارامتر version فراموش شده
اگر کاربر ?version=v1 را نفرستد، DEFAULT_VERSION استفاده میشود. اگر DEFAULT_VERSION را تنظیم نکرده باشید، خطا میگیرید.
خطا: Version not allowed
کاربر ?version=v5 فرستاده اما ALLOWED_VERSIONS فقط ['v1', 'v2'] را دارد.
تمرین این بخش
یک API ساده با مدل Product بسازید. دو نسخه پیادهسازی کنید:
نسخه v1: فیلدهای id و name
نسخه v2: فیلدهای id، title، price و stock
از QueryParameterVersioning استفاده کنید. در مرورگر آدرسهای زیر را تست کنید و خروجی را ببینید:
http://127.0.0.1:8000/api/products/?version=v1
http://127.0.0.1:8000/api/products/?version=v2
http://127.0.0.1:8000/api/products/ (بدون پارامتر - DEFAULT_VERSION)
روش NamespaceVersioning
این روش کمی با سه روش قبلی فرق دارد. در روشهای قبلی، نسخه را در آدرس، هدر، یا پارامتر میگذاشتید. اما اینجا از یک قابلیت خود جنگو استفاده میکنید: namespace در مسیرها.
این روش چگونه کار میکند؟
در فایل urls.py، به هر گروه از مسیرها یک فضای نام (namespace) میدهید. اسم این فضا همان نسخه API است.
# urls.py اصلی
from django.urls import path, include
urlpatterns = [
path('api/v1/', include(('myapp.urls', 'myapp'), namespace='v1')),
path('api/v2/', include(('myapp.urls', 'myapp'), namespace='v2')),
]
آدرسهای نهایی:
GET /api/v1/products/
GET /api/v2/products/
از نظر کاربر، دقیقاً مثل روش URLPathVersioning به نظر میرسد. نسخه در آدرس است. اما از نظر پیادهسازی در DRF، تفاوت دارد.
تفاوت با URLPathVersioning
در روش URLPathVersioning، شما یک urlpatterns داشتید و یک پارامتر <str:version> در مسیر. در این روش، شما دو path جداگانه دارید. هر کدام به یک namespace متفاوت اشاره میکنند. هر دو به یک فایل urls.py (اپ یکسان) اشاره میکنند، اما با فضای نام متفاوت.
قدم اول: تنظیم در settings.py
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
}
قدم دوم: تنظیم مسیرها در urls.py اصلی
# urls.py اصلی
from django.urls import path, include
urlpatterns = [
path('api/v1/', include(('myapp.urls', 'myapp'), namespace='v1')),
path('api/v2/', include(('myapp.urls', 'myapp'), namespace='v2')),
]
نکته مهم: فرمت include(('app_name.urls', 'app_name'), namespace='...') به جنگو میگوید که این مسیرها به اپ myapp تعلق دارند و فضای نام آنها v1 یا v2 است.
قدم سوم: فایل urls.py اپ کاملاً معمولی
در اپ خود، هیچ تغییری نمیدهید:
# myapp/urls.py
from django.urls import path
from .views import ProductListView
app_name = 'myapp' # این خط مهم است
urlpatterns = [
path('products/', ProductListView.as_view(), name='product-list'),
]
قدم چهارم: استفاده از نسخه در ویو
باز هم مثل روشهای قبل:
# views.py
from rest_framework.generics import ListAPIView
from .models import Product
from .serializers import ProductSerializerV1, ProductSerializerV2
class ProductListView(ListAPIView):
queryset = Product.objects.all()
def get_serializer_class(self):
version = self.request.version
if version == 'v2':
return ProductSerializerV2
return ProductSerializerV1
مزایای NamespaceVersioning
مزیت اول: جداسازی کامل در سطح URLconf
هر نسخه یک namespace جدا دارد. میتوانید در آینده مسیرهای هر نسخه را کاملاً جداگانه تغییر دهید. بدون تأثیر روی نسخه دیگر.
مزیت دوم: استفاده از قابلیت reverse جنگو
در کد خود، میتوانید با reverse('v1:product-list') یا reverse('v2:product-list') به مسیرهای نسخههای مختلف ارجاع دهید. خیلی تمیز و خوانا.
مزیت سوم: امکان تغییر ساختار مسیر در هر نسخه
در روشهای دیگر، ساختار مسیر برای همه نسخهها یکسان است. اینجا میتوانید در نسخه دوم مسیرها را عوض کنید:
urlpatterns = [
path('api/v1/', include(('myapp.urls_v1', 'myapp'), namespace='v1')),
path('api/v2/', include(('myapp.urls_v2', 'myapp'), namespace='v2')),
]
نسخه اول از urls_v1.py استفاده میکند و نسخه دوم از urls_v2.py. هرکدام مسیرهای اختصاصی خود را دارند.
معایب NamespaceVersioning
عیب اول: urls.py شلوغ میشود
اگر ۵ نسخه داشته باشید، ۵ path جداگانه در urls.py اصلی مینویسید.
عیب دوم: کمی پیچیدهتر از روشهای دیگر
برای درک آن باید با مفهوم namespace در جنگو آشنا باشید.
عیب سوم: نمیتوانید از router به راحتی استفاده کنید
اگر از DefaultRouter استفاده میکنید، باید برای هر نسخه یک router جداگانه بسازید و register کنید.
| ویژگی | URLPath | AcceptHeader | QueryParam | Namespace |
| نسخه کجاست؟ | مسیر | هدر | پارامتر | مسیر (با namespace) |
| تست در مرورگر | آسان | سخت | خیلی آسان | آسان |
| تغییر ساختار مسیر در نسخهها | خیر | خیر | خیر | بله |
| استفاده از reverse | محدود | محدود | محدود | عالی |
| پیچیدگی پیادهسازی | کم | کم | کم | متوسط |
مثال عملی با دو نسخه و مسیرهای متفاوت
نسخه اول (v1):
# myapp/urls_v1.py
urlpatterns = [
path('products/', ProductListView.as_view()),
]
نسخه دوم (v2):
# myapp/urls_v2.py
urlpatterns = [
path('products/', ProductListViewV2.as_view()),
path('products/bulk-create/', BulkCreateView.as_view()), # مسیر جدید در نسخه ۲
]
تنظیمات اصلی:
urlpatterns = [
path('api/v1/', include(('myapp.urls_v1', 'myapp'), namespace='v1')),
path('api/v2/', include(('myapp.urls_v2', 'myapp'), namespace='v2')),
]
چه موقع از NamespaceVersioning استفاده کنیم؟
| سناریو | توصیه |
| پروژه بزرگ با چند تیم | عالی است |
| نسخهها ساختار مسیر متفاوتی دارند | عالی است |
| نیاز به reverse در کد دارید | عالی است |
| پروژه کوچک با ۲ نسخه | زیادی است |
| تازه با DRF آشنا شدهاید | بعدا یاد بگیرید |
تمرین
یک پروژه با دو نسخه از API محصولات بسازید:
- نسخه v1: مسیر products/ (لیست و جزئیات)
- نسخه v2: مسیر products/ (لیست و جزئیات) + مسیر products/bulk/ (ساخت دستهجمعی)
از NamespaceVersioning استفاده کنید. هر نسخه فایل urls.py مخصوص خود را داشته باشد.
روش HostNameVersioning
تا الان نسخه را در مسیر آدرس، هدر، پارامتر، یا namespace دیدید. این بار نسخه را در دامنه (hostname) قرار میدهیم.
آدرس این شکلی میشود:
http://v1.api.example.com/products/
http://v2.api.example.com/products/
هر نسخه یک زیردامنه جداگانه دارد. آدرسها کاملاً تمیز هستند. خبری از v1/ در مسیر نیست.
این روش بیشتر در پروژههای خیلی بزرگ و سازمانی استفاده میشود. جایی که شرکت میخواهد ترافیک نسخههای مختلف را به سرورهای کاملاً جداگانه هدایت کند.
تفاوت با روشهای قبلی
در روشهای قبلی، همه نسخهها روی یک سرور و یک دامنه بودند. فقط آدرس یا هدر فرق میکرد.
در این روش، هر نسخه میتواند روی یک سرور مجزا اجرا شود. مثلاً v1 روی سرور قدیمی، v2 روی سرور جدید.
قدم اول: تنظیم در settings.py
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.HostNameVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2', 'v3'],
'VERSION_PARAM': 'version',
}
قدم دوم: تنظیم مسیرها (urls.py)
مسیرها کاملاً معمولی هستند. چون نسخه در دامنه است، نه در مسیر.
# urls.py اصلی
from django.urls import path, include
urlpatterns = [
path('api/', include('myapp.urls')),
]
# myapp/urls.py
from django.urls import path
from .views import ProductListView
urlpatterns = [
path('products/', ProductListView.as_view(), name='product-list'),
]
آدرس نهایی بدون دامنه:
GET /api/products/
اما دامنه متفاوت است:
http://v1.example.com/api/products/
http://v2.example.com/api/products/
قدم سوم: استفاده از نسخه در ویو
مثل روشهای دیگر:
# views.py
from rest_framework.generics import ListAPIView
from .models import Product
from .serializers import ProductSerializerV1, ProductSerializerV2
class ProductListView(ListAPIView):
queryset = Product.objects.all()
def get_serializer_class(self):
version = self.request.version
if version == 'v2':
return ProductSerializerV2
return ProductSerializerV1
نحوه تست در محیط لوکال
تست HostNameVersioning روی localhost کمی سخت است. چون localhost زیردامنه v1.localhost و v2.localhost ندارد.
برای تست در محیط توسعه، دو راه دارید.
راه اول: اضافه کردن به hosts file
فایل hosts سیستم خود را ویرایش کنید (در ویندوز: C:\Windows\System32\drivers\etc\hosts، در مک/لینوکس: /etc/hosts):
127.0.0.1 v1.example.local
127.0.0.1 v2.example.local
سپس با آدرسهای زیر تست کنید:
http://v1.example.local:8000/api/products/
http://v2.example.local:8000/api/products/
راه دوم: تغییر هدر Host در Postman
در Postman، به برگه Headers بروید. یک هدر اضافه کنید:
| Value | Key |
| https://v1.example.com/ | Host |
سپس درخواست را به http://127.0.0.1:8000/api/products/ بفرستید. Postman هدر Host را به v1.example.com تغییر میدهد و DRF آن را میخواند.
مزایای HostNameVersioning
جداسازی کامل در سطح DNS
میتوانید v1.api.example.com به یک سرور، v2.api.example.com به سرور دیگر اشاره کند. در روشهای دیگر، همه نسخهها روی یک سرور هستند.
آدرس کاملاً تمیز
هیچ v1/ یا v1= یا هدر اضافی در کار نیست. آدرس کوتاه و خواناست.
کش کردن مستقل
CDNها میتوانند هر زیردامنه را جداگانه کش کنند. اگر نسخه دوم را بروزرسانی کردید، روی کش نسخه اول تأثیر نمیگذارد.
امنیت بیشتر
میتوانید برای هر نسخه قوانین امنیتی جداگانه در فایروال یا لودبالانسر تعریف کنید.
معایب HostNameVersioning
پیچیدگی در محیط توسعه
تست روی localhost ساده نیست. باید hosts را تغییر دهید یا از Postman با هدر سفارشی استفاده کنید.
نیاز به تنظیمات DNS
برای محیط تولید، باید برای هر نسخه یک زیردامنه جدید در DNS تعریف کنید.
مشکل با SSL/TLS
برای هر زیردامنه به گواهی SSL جداگانه نیاز دارید. یا باید از گواهی wildcard (مثل *.api.example.com) استفاده کنید.
مهاجرت سختتر کاربران
کاربر باید آدرس متفاوتی را در کد خود وارد کند. برخلاف روش AcceptHeaderVersioning که آدرس یکسان میماند.
| ویژگی | URLPath | AcceptHeader | QueryParam | Namespace | HostName |
| نسخه کجاست؟ | مسیر | هدر | پارامتر | namespace | زیردامنه |
| تست در لوکال | آسان | متوسط | آسان | آسان | سخت |
| جداسازی سرورها | خیر | خیر | خیر | خیر | بله |
| نیاز به DNS | خیر | خیر | خیر | خیر | بله |
| آدرس تمیز | نه | بله | نسبتا | نه | بله |
| پیچیدگی | کم | کم | کم | متوسط | زیاد |
چه موقع از HostNameVersioning استفاده کنیم؟
| توصیه | سناریو |
| پروژه خیلی بزرگ با چند سرور | عالی است |
| سازمانی که کنترل کامل روی زیرساخت دارد | عالی است |
| API عمومی با ترافیک بالا | خوب است |
| استارتاپ کوچک با یک سرور | زیادی است |
| تازه با DRF آشنا شدهاید | اصلاً توصیه نمیشود |
مثال عملی در تنظیمات Nginx
اگر از Nginx استفاده میکنید، میتوانید درخواستهای هر زیردامنه را به سرور متفاوتی هدایت کنید:
server {
server_name v1.api.example.com;
location / {
proxy_pass http://backend_v1:8000;
}
}
server {
server_name v2.api.example.com;
location / {
proxy_pass http://backend_v2:8000;
}
}
تمرین
اگر میخواهید این روش را امتحان کنید، مراحل زیر را انجام دهید:
۱. فایل hosts سیستم خود را ویرایش کنید و دو خط زیر را اضافه کنید:
127.0.0.1 v1.api.local
127.0.0.1 v2.api.local
۲. یک پروژه DRF ساده با مدل Product بسازید.
۳. HostNameVersioning را در settings.py فعال کنید.
۴. دو ویو مشابه بنویسید اما سریالایزرهای متفاوت برای نسخههای مختلف.
۵. در مرورگر آدرسهای زیر را تست کنید:
http://v1.api.local:8000/api/products/
http://v2.api.local:8000/api/products/
جمعبندی درس نسخهبندی API در DRF
در این درس، با یکی از مفاهیم حیاتی برای نگهداری API در بلندمدت آشنا شدید: نسخهبندی. یاد گرفتید چرا بدون نسخهبندی، هر تغییری در API میتواند باعث خرابی اپلیکیشنهای مشتریان شود. و چطور نسخهبندی به شما اجازه میدهد API خود را بهبود ببخشید، بدون اینکه به کاربران قدیمی آسیب بزنید. پنج روش مختلف نسخهبندی را مرور کردید.
- URLPathVersioning ساده و رایج. نسخه در آدرس. تست آسان. مناسب برای اکثر پروژهها.
- AcceptHeaderVersioning حرفهایتر. نسخه در هدر. آدرس تمیز میماند. برای APIهایی که کنترل کلاینتها را دارید، عالی است.
- QueryParameterVersioning سادهترین روش برای تست. نسخه به عنوان پارامتر کوئری. برای مرحله توسعه و APIهای داخلی مناسب است.
- NamespaceVersioning با استفاده از قابلیت namespace جنگو. به شما اجازه میدهد ساختار مسیر نسخههای مختلف را جداگانه تعریف کنید. برای پروژههای بزرگ با تیمهای مختلف.
- HostNameVersioning پیشرفتهترین روش. نسخه در زیردامنه. قابلیت هدایت ترافیک هر نسخه به سرور جداگانه. برای پروژههای خیلی بزرگ و سازمانی.
از این درس چه چیزی باید به خاطر بسپارید؟
- نسخهبندی را از روز اول در نظر بگیرید. حتی اگر الان فقط یک نسخه دارید. بعداً اضافه کردن آن سختتر است.
- برای شروع، URLPathVersioning سادهترین و مطمئنترین انتخاب است.
- اگر API شما عمومی است و مشتریان خارجی دارد، هرگز یک نسخه را به یکباره حذف نکنید. حداقل ۶ ماه تا یک سال فرصت مهاجرت بدهید.
- ALLOWED_VERSIONS را حتماً تنظیم کنید. از درخواستهای نسخههای نامعتبر جلوگیری میکند.
- DEFAULT_VERSION را همیشه مشخص کنید. برای مواقعی که کاربر نسخهای نفرستاده است.
کدام روش را انتخاب کنیم؟
| پروژه شما | روش پیشنهادی |
| پروژه کوچک، تازه شروع کردهاید | URLPathVersioning |
| API عمومی با مشتریان متعدد | URLPathVersioning |
| API داخلی شرکت | AcceptHeaderVersioning یا QueryParameterVersioning |
| پروژه بزرگ با چند تیم | NamespaceVersioning |
| سازمان بزرگ با زیرساخت جداگانه برای هر نسخه | HostNameVersioning |
درس بعدی چیست؟
حالا API شما نسخهبندی شده است. کاربران قدیمی و جدید کنار هم کار میکنند. اما یک مشکل دیگر باقی مانده است، مستندات. اگر API شما مستندات خوبی نداشته باشد، هیچکس نمیداند چطور از آن استفاده کند. توسعهدهندهها مجبور میشوند کد شما را بخوانند. وقتشان تلف میشود. احتمال دارد از API شما دوری کنند.
در درس بعدی، یاد میگیرید چطور به صورت خودکار برای API خود مستندات حرفهای تولید کنید. با ابزارهایی مثل drf-yasg (Swagger) و Spectacular. بدون اینکه یک خط اضافه بنویسید.