تا الان چندین 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. بدون اینکه یک خط اضافه بنویسید.