تا الان ابزارهای DRF را یکی یکی یاد گرفتید. سریالایزر، ویوهای مختلف، احراز هویت، فیلتر، صفحه‌بندی، نسخه‌بندی و مستندسازی. اما شرکت‌ها فقط ابزار بلد بودن را نمی‌خرند. آنها راه حل می‌خرند. آنها کد تمیز و قابل نگهداری می‌خرند. آنها امنیت و کارایی می‌خرند. این درس پلی است بین «بلدم» و «می‌توانم در محیط واقعی کار کنم». اینجا چند موضوع خیلی کاربردی را جمع کرده‌ام. موضوعاتی که در مصاحبه‌های شغلی از شما می‌پرسند. در کدهای تولیدی شرکت‌ها مدام می‌بینید. و در هیچکدام از درس‌های قبلی به تفصیل به آنها پرداخته نشده است.

  • اول: مقایسه نهایی APIView، Generic Views و ViewSet. کدام را کی استفاده کنیم؟ راهنمایی برای انتخاب درست در پروژه‌های واقعی.
  • دوم: مدیریت خطاها و ارورهای استاندارد. هیچ API بدون خطا کار نمی‌کند. اما مهم این است که خطاها را چطور به کلاینت برگردانید. طوری که قابل فهم باشد و امنیت را هم حفظ کند.
  • سوم: سریالایزر تو در تو (Nested Serializer). فرض کنید یک مقاله چند کامنت دارد. یا یک سفارش چند محصول. چطور همه را یکجا برگردانید؟ بدون نوشتن کد اضافی.
  • چهارم: محدودیت درخواست (Throttling). اگر کاربری هزار بار در یک دقیقه به API شما حمله کند، چه کار می‌کنید؟ بدون این قابلیت، سرورتان خیلی زود از کار می‌افتد.
  • پنجم: ذخیره فایل با DRF. آپلود عکس پروفایل، فایل ضمیمه مقاله، یا هر فایل دیگری. با چالش‌های خاص خودش مثل امنیت و حجم.

این درس آخر، شما را برای ورود به بازار کار آماده می کند.

مقایسه نهایی: APIView vs Generic Views vs ViewSet (راهنمای انتخاب)

حالا که با هر سه روش کار کرده‌اید، وقتش رسیده یک جمع‌بندی نهایی داشته باشیم. اینکه هر کدام را چه موقع استفاده کنید، خیلی مهم است.

APIView – کنترل کامل

APIView پایه همه چیز است. به شما کنترل کامل روی منطق ویو می‌دهد. هر متدی را خودتان می‌نویسید. هر خطایی را خودتان مدیریت می‌کنید.

چه موقع باید استفاده کنید؟

  • منطق API شما غیرعادی و خاص است.
  • نیاز به کنترل کامل روی جریان درخواست و پاسخ دارید.
  • نمی‌خواهید Generic Views یا ViewSet شما را محدود کند.
  • پروژه شما کوچک است و فقط چند API ساده دارد.

مثال واقعی: یک API که باید قبل از برگرداندن داده، چند سرویس خارجی را صدا بزند و نتایج را ترکیب کند.

Generic Views – تعادل بین سادگی و کنترل

Generic Views کدهای تکراری CRUD را برای شما می‌نویسند. اما هنوز هم می‌توانید با بازنویسی متدها، رفتار را تغییر دهید.

چه موقع باید استفاده کنید؟

  • نیاز به عملیات استاندارد CRUD دارید.
  • پروژه شما متوسط است و چندین مدل مشابه دارد.
  • می‌خواهید کد کمی بنویسید اما کنترل خود را از دست ندهید.
  • نیاز به فیلتر، جستجو و صفحه‌بندی دارید.

مثال واقعی: یک فروشگاه اینترنتی با مدل‌های محصول، دسته‌بندی و سفارش. هر کدام نیاز به لیست، ساخت، جزئیات و حذف دارند.

ViewSet – حداکثر کاهش کد

ViewSet همه عملیات‌های CRUD را در یک کلاس جمع می‌کند. همراه با Router، دیگر خبری از نوشتن urlpatterns نیست.

چه موقع استفاده کنید؟

  • پروژه شما بزرگ است و مدل‌های زیادی دارد.
  • همه عملیات‌های CRUD را نیاز دارید.
  • می‌خواهید کد خود را جمع‌وجور و قابل نگهداری کنید.
  • از Router برای خودکارسازی مسیرها استفاده می‌کنید.

مثال واقعی: یک پنل مدیریت با ۲۰ مدل مختلف که هر کدام نیاز به عملیات کامل دارند.

جدول مقایسه سریع

ویژگی APIView Generic Views ViewSet
حجم کد زیاد متوسط کم
کنترل روی منطق کامل متوسط کم
زمان یادگیری کم متوسط زیاد
مناسب برای پروژه کوچک بله بله زیادی است
مناسب برای پروژه بزرگ خیر بله بله
نیاز به نوشتن urlpatterns دستی دستی خودکار با Router
اضافه کردن متد سفارشی آسان متوسط آسان با @action

 

راهنمای نهایی انتخاب

مرحله اول: چند API دارید؟ اگر کمتر از ۵ تا، APIView یا Generic Views کافی است. اگر بیشتر از ۱۰ تا، ViewSet به دردتان می‌خورد.

مرحله دوم: منطق API شما استاندارد است یا خاص؟ اگر استاندارد است، Generic Views یا ViewSet. اگر خاص است، APIView.

مرحله سوم: چه کسی قرار است کد شما را نگهداری کند؟ اگر خودتان هستید، هر روشی راحت‌تر است. اگر تیم بزرگ است، ViewSet با Router استاندارد طلایی شرکت‌هاست.

مرحله چهارم: به قابلیت‌های اضافه نیاز دارید؟ فیلتر، جستجو، صفحه‌بندی؟ Generic Views و ViewSet اینها را از قبل دارند.

تجربه شخصی

در اکثر شرکت‌هایی که کار کرده‌ام، قانون نانوشته این بوده: برای APIهای جدید، از ViewSet استفاده کن. مگر اینکه دلیل خوبی برای استفاده از روش دیگر داشته باشی. دلیل ساده است: ViewSet کد را یکدست می‌کند. همه می‌دانند هر ViewSet چه عملیاتی دارد. Router مسیرها را استاندارد می‌سازد. و @action برای متدهای سفارشی، کار را تمیز نگه می‌دارد.

مدیریت خطاها و ارورهای استاندارد در DRF

هیچ API بدون خطا کار نمی‌کند. داده نامعتبر می‌رسد. منبع پیدا نمی‌شود. کاربر دسترسی ندارد. سرور دچار مشکل می‌شود.

مهم این نیست که خطا رخ نمی‌دهد. مهم این است که وقتی خطا رخ داد، چه پاسخی به کلاینت بدهید.

خطاهای استاندارد HTTP

DRF بیشتر خطاهای رایج را خودکار مدیریت می‌کند. اما شما باید بدانید هر خطا چه کدی دارد و چه معنایی می‌دهد.

  • ۲۰۰ OK – درخواست موفق. معمولاً برای GET، PUT، PATCH.
  • ۲۰۱ Created – منبع جدید ساخته شد. برای POST موفق.
  • ۲۰۴ No Content – درخواست موفق، اما محتوایی برای برگرداندن نیست. برای DELETE.
  • ۴۰۰ Bad Request – داده نامعتبر. مثلاً فیلد اجباری خالی است.
  • ۴۰۱ Unauthorized – کاربر احراز هویت نشده. توکن نفرستاده یا نامعتبر است.
  • ۴۰۳ Forbidden – کاربر احراز هویت شده، اما اجازه انجام این کار را ندارد.
  • ۴۰۴ Not Found – منبع با این شناسه وجود ندارد.
  • ۴۲۹ Too Many Requests – کاربر تعداد درخواست مجاز را رد کرده. (Throttling)
  • ۵۰۰ Internal Server Error – خطایی در سمت سرور رخ داده. معمولاً باگ در کد شما.

مدیریت خطا در APIView

در APIView خودتان خطاها را مدیریت می‌کنید. این هم خوب است هم بد. خوب چون کنترل دارید. بد چون ممکن است یک جا را فراموش کنید.

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.core.exceptions import ObjectDoesNotExist

class ProductDetailView(APIView):
    def get(self, request, pk):
        try:
            product = Product.objects.get(pk=pk)
        except ObjectDoesNotExist:
            return Response(
                {'error': 'محصولی با این شناسه وجود ندارد'},
                status=status.HTTP_404_NOT_FOUND
            )
        serializer = ProductSerializer(product)
        return Response(serializer.data)

مدیریت خطا در Generic Views و ViewSet

در Generic Views و ViewSet، بیشتر خطاها خودکار مدیریت می‌شوند. مثلاً اگر منبع پیدا نشود، خودکار ۴۰۴ برمی‌گرداند.

اما برای خطاهای خاص، باید handle_exception را بازنویسی کنید:

from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)
    
    if response is not None:
        response.data['status_code'] = response.status_code
        response.data['message'] = 'خطایی رخ داده است'
        
        # اگر خطای اعتبارسنجی بود
        if response.status_code == status.HTTP_400_BAD_REQUEST:
            response.data['validation_errors'] = response.data.get('detail', {})
            
    return response

سپس در settings.py:

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler',
}

ساختار استاندارد پاسخ خطا

یک ساختار ثابت برای پاسخ خطا به کلاینت کمک می‌کند. می‌توانید فرمت زیر را انتخاب کنید:

{
    "success": false,
    "error_code": "PRODUCT_NOT_FOUND",
    "message": "محصولی با این شناسه وجود ندارد",
    "details": {}
}

برای خطاهای اعتبارسنجی:

{
    "success": false,
    "error_code": "VALIDATION_ERROR",
    "message": "داده ارسالی معتبر نیست",
    "details": {
        "title": ["این فیلد نمی‌تواند خالی باشد"],
        "price": ["قیمت باید بزرگتر از صفر باشد"]
    }
}

خطاهای اعتبارسنجی در Serializer

وقتی داده نامعتبر به Serializer می‌رسد، DRF خودکار خطاهای اعتبارسنجی را برمی‌گرداند. اما می‌توانید پیام‌ها را سفارشی کنید:

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['name', 'price']
    
    def validate_price(self, value):
        if value <= 0:
            raise serializers.ValidationError("قیمت باید بزرگتر از صفر باشد")
        return value
    
    def validate(self, data):
        if data['price'] > 10000 and not data.get('is_premium'):
            raise serializers.ValidationError(
                "محصولات بالای ۱۰ هزار تومان نیاز به تایید ویژه دارند"
            )
        return data

نکته مهم: لو دادن اطلاعات حساس

هرگز جزییات خطاهای سرور (۵۰۰) را به کلاینت نشان ندهید. در محیط تولید، DRF خطاهای سرور را به صورت عمومی نشان نمی‌دهد. اما خودتان هم در کد مراقب باشید.

# بد
try:
    product = Product.objects.get(pk=pk)
except Exception as e:
    return Response({'error': str(e)}, status=500)  # جزییات خطا را لو می‌دهد

# خوب
try:
    product = Product.objects.get(pk=pk)
except Product.DoesNotExist:
    return Response({'error': 'پیدا نشد'}, status=404)
except Exception:
    return Response({'error': 'خطای سرور'}, status=500)  # جزئیات پنهان

تمرین این بخش

یک ویو بنویسید که:

  • اگر محصول پیدا نشد، خطای ۴۰۴ با پیام مناسب برگرداند.
  • اگر قیمت منفی بود، خطای ۴۰۰ با توضیح برگرداند.
  • اگر کاربر ادمین نبود و سعی در حذف محصول کرد، خطای ۴۰۳ برگرداند.

سریالایزر تو در تو (Nested Serializer) – نمایش و ذخیره همزمان

تا الان Serializerها را برای مدل‌های ساده می‌نوشتید. اما در دنیای واقعی، مدل‌ها به هم مربوط هستند. به این مثال توجه کنید.

یک مقاله چند کامنت دارد. یک سفارش چند محصول دارد. یک کاربر چند آدرس دارد. حالا کاربر می‌خواهد در یک درخواست، هم مقاله را ببیند و هم همه کامنت‌هایش را. یا در یک درخواست، هم سفارش جدید ثبت کند و هم محصولات آن را. اینجا Nested Serializer وارد می‌شود.

Nested Serializer چیست؟

یعنی یک Serializer را داخل Serializer دیگر استفاده کنید. مدل اصلی را سریالایز می‌کند و در همان لحظه، مدل‌های مربوط به آن را هم می‌خواند.

مثال: مقاله و کامنت‌ها

فرض کنید دو مدل دارید. یک مقاله و چند کامنت که به آن مقاله متصل هستند.

# models.py
class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')
    text = models.TextField()
    author = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

حالا یک Serializer برای کامنت می‌نویسیم:

# serializers.py
class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'text', 'author', 'created_at']

و برای مقاله، کامنت‌ها را تو در تو قرار می‌دهیم:

class ArticleSerializer(serializers.ModelSerializer):
    comments = CommentSerializer(many=True, read_only=True)
    
    class Meta:
        model = Article
        fields = ['id', 'title', 'content', 'created_at', 'comments']

حالا وقتی یک مقاله را می‌خوانید، خروجی به این شکل است:

{
    "id": 1,
    "title": "آموزش DRF",
    "content": "...",
    "created_at": "2025-01-15T10:00:00Z",
    "comments": [
        {
            "id": 1,
            "text": "بسیار عالی بود",
            "author": "علی",
            "created_at": "2025-01-15T11:00:00Z"
        },
        {
            "id": 2,
            "text": "ممنون از شما",
            "author": "مریم",
            "created_at": "2025-01-15T12:00:00Z"
        }
    ]
}

ساخت همزمان مقاله و کامنت‌ها

گاهی می‌خواهید کاربر در یک درخواست، هم مقاله را بسازد و هم کامنت‌های اولیه را اضافه کند.

برای این کار باید create را در Serializer بازنویسی کنید:

class ArticleSerializer(serializers.ModelSerializer):
    comments = CommentSerializer(many=True, required=False)
    
    class Meta:
        model = Article
        fields = ['id', 'title', 'content', 'created_at', 'comments']
    
    def create(self, validated_data):
        comments_data = validated_data.pop('comments', [])
        article = Article.objects.create(**validated_data)
        
        for comment_data in comments_data:
            Comment.objects.create(article=article, **comment_data)
        
        return article

حالا کاربر می‌تواند با یک درخواست POST، هم مقاله و هم کامنت‌هایش را بسازد:

{
    "title": "آموزش DRF",
    "content": "...",
    "comments": [
        {"text": "عالی بود", "author": "علی"},
        {"text": "ممنون", "author": "مریم"}
    ]
}

Nested Serializer برای ForeignKey معکوس

related_name='comments' در مدل Comment به ما اجازه می‌دهد با نام comments به کامنت‌های یک مقاله دسترسی داشته باشیم. اگر related_name را تعریف نکرده باشید، نام پیش‌فرض comment_set است.

Nested Serializer برای Forward ForeignKey

گاهی می‌خواهید اطلاعات مدل والد را درون مدل فرزند نشان دهید. مثلاً در سریالایزر کامنت، اطلاعات مقاله را هم برگردانید.

class ArticleBriefSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ['id', 'title']

class CommentSerializer(serializers.ModelSerializer):
    article = ArticleBriefSerializer(read_only=True)
    
    class Meta:
        model = Comment
        fields = ['id', 'text', 'author', 'created_at', 'article']

نکات مهم Nested Serializer

خواندن آسان است. فقط read_only=True اضافه می‌کنید و تمام.

نوشتن (ساخت/بروزرسانی) سخت‌تر است. باید create و update را بازنویسی کنید.

مواظب عملکرد باشید. Nested Serializer می‌تواند تعداد کوئری‌های دیتابیس را زیاد کند. برای هر مقاله، یک کوئری مجزا برای گرفتن کامنت‌ها می‌زند. برای حل این مشکل از prefetch_related در ویو استفاده کنید:

class ArticleListView(ListAPIView):
    queryset = Article.objects.prefetch_related('comments').all()
    serializer_class = ArticleSerializer

چه موقع از Nested Serializer استفاده کنیم؟

سناریو توصیه
نمایش روابط یک به چند (یک مقاله با چند کامنت) عالی است
نمایش روابط چند به چند (یک سفارش با چند محصول) عالی است
ساخت همزمان مدل والد و فرزند خوب است، ولی کد بیشتری می‌خواهد
بروزرسانی همزمان پیچیده است، کمتر توصیه می‌شود

محدودیت درخواست (Throttling) – محافظت از API در برابر حملات

فرض کنید API شما را یک نفر پیدا کرده است. نه از روی علاقه، بلکه برای آسیب زدن. شروع می‌کند به فرستادن هزاران درخواست در ثانیه. سرور شما اذیت می‌شود. کند می‌شود. ممکن است از کار بیفتد. یا شاید کاربر عادی شما اشتباهاً دکمه را چند بار می‌زند. نیازی نیست هر بار سرور پاسخ بدهد.

راه حل: محدودیت درخواست یا Throttling.

Throttling چیست؟

یعنی مشخص کنید هر کاربر در یک بازه زمانی چند بار می‌تواند به API شما درخواست بدهد. اگر از حد مجاز گذشت، DRF خودکار خطای ۴۲۹ Too Many Requests برمی‌گرداند.

انواع Throttle در DRF

DRF چهار نوع throttle آماده دارد.

  • AnonRateThrottle: محدودیت برای کاربران مهمان (لاگین نشده). معمولاً کمتر از کاربران عضو.
  • UserRateThrottle: محدودیت برای کاربران لاگین شده. بر اساس شناسه کاربر.
  • ScopedRateThrottle: محدودیت متفاوت برای هر ویو یا هر endpoint.
  • CustomRateThrottle: خودتان بنویسید. برای نیازهای خاص.

قدم اول: تنظیم در settings.py

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '10/minute',    # کاربر مهمان: ۱۰ درخواست در دقیقه
        'user': '100/hour',     # کاربر عضو: ۱۰۰ درخواست در ساعت
    }
}

فرمت DEFAULT_THROTTLE_RATES: عدد + / + بازه زمانی. بازه‌های مجاز: second، minute، hour، day.

قدم دوم: استفاده در ویو

با تنظیمات بالا، همه ویوهای شما به طور خودکار محدودیت می‌خورند. بدون نیاز به کار اضافه. اگر می‌خواهید یک ویو خاص محدودیت متفاوتی داشته باشد:

from rest_framework.throttling import UserRateThrottle

class BurstUserRateThrottle(UserRateThrottle):
    rate = '5/minute'

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    throttle_classes = [BurstUserRateThrottle]  # محدودیت شدیدتر

قدم سوم: throttle اختصاصی برای یک ویو

می‌توانید هم تنظیمات سراسری داشته باشید و هم برای یک ویو خاص محدودیت جدید تعریف کنید. DRF همه را با هم جمع می‌کند.

class SensitiveAPIView(APIView):
    throttle_classes = [UserRateThrottle]  # محدودیت اضافی
    # با تنظیمات سراسری جمع می‌شود

ScopedRateThrottle – throttle متفاوت برای هر endpoint

اگر می‌خواهید هر ویو محدودیت جداگانه داشته باشد:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.ScopedRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'products': '100/hour',
        'orders': '50/hour',
        'login': '5/minute',
    }
}

در ویوها، throttle_scope را مشخص کنید:

class ProductListView(ListAPIView):
    throttle_scope = 'products'
    ...

class OrderListView(ListAPIView):
    throttle_scope = 'orders'
    ...

Throttle سفارشی برای نیازهای خاص

گاهی محدودیت بر اساس چیزی غیر از کاربر است. مثلاً بر اساس IP آدرس. یا بر اساس نوع پلن کاربری.

from rest_framework.throttling import BaseThrottle

class CustomIPThrottle(BaseThrottle):
    def allow_request(self, request, view):
        ip = request.META.get('REMOTE_ADDR')
        # منطق خودتان برای محدودیت بر اساس IP
        # اگر اجازه دارد: return True
        # اگر محدود شده: return False
    
    def wait(self):
        # چند ثانیه باید صبر کند
        return 60

سپس در ویو یا تنظیمات سراسری استفاده کنید.

خطای ۴۲۹ Too Many Requests

وقتی کاربر از حد مجاز بگذرد، DRF خودکار پاسخ زیر را برمی‌گرداند:

{
    "detail": "درخواست‌ها محدود شده است. ۶۰ ثانیه دیگر صبر کنید."
}

در هدر پاسخ هم Retry-After فرستاده می‌شود. کلاینت می‌تواند بخواند و چند ثانیه صبر کند.

نکات مهم در تنظیم نرخ

  • برای APIهای عمومی، anon: 10/minute شروع خوبی است.
  • برای کاربران عادی، user: 100/hour کافی است.
  • برای endpointهای حساس مثل لاگین، 5/minute منطقی است.
  • برای APIهای پولی، می‌توانید بر اساس پلن کاربری throttleهای متفاوت بدهید.

چه موقع از Throttling استفاده کنیم؟

سناریو توصیه
API عمومی با کاربران مهمان حتماً AnonRateThrottle فعال کنید
API لاگین و ثبت‌نام محدودیت شدید (مثلاً ۵ بار در دقیقه)
APIهای سنگین (گزارش‌گیری) محدودیت کمتر (مثلاً ۱۰ بار در ساعت)
APIهای سبک (لیست محصولات) محدودیت بیشتر (مثلاً ۱۰۰ بار در دقیقه)

تمرین 

تنظیمات زیر را در پروژه خود پیاده‌سازی کنید:

  • کاربران مهمان: ۵ درخواست در دقیقه
  • کاربران عضو: ۵۰ درخواست در ساعت
  • endpoint لاگین: ۳ درخواست در دقیقه (با ScopedRateThrottle)

سپس با Postman بیش از حد مجاز درخواست بفرستید و خطای ۴۲۹ را ببینید.

نحوه ذخیره فایل‌ها (آپلود عکس و فایل) در DRF

تا الان با متن و عدد کار کرده‌اید. اما خیلی از APIها نیاز به دریافت فایل هم دارند. عکس پروفایل کاربر، تصویر محصول، فایل ضمیمه مقاله، یا هر فایل دیگری. خوشبختانه DRF آپلود فایل را به خوبی پشتیبانی می‌کند.

قدم اول: تنظیمات مدل

اولین قدم، اضافه کردن فیلد فایل به مدل است. دو نوع فیلد اصلی داریم:

  • FileField: برای هر نوع فایلی (PDF، عکس، ویدیو، هر چیزی)
  • ImageField: فقط برای عکس. اعتبارسنجی خودکار دارد که فایل حتماً عکس باشد.
# models.py
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.IntegerField()
    image = models.ImageField(upload_to='products/', null=True, blank=True)
    catalog = models.FileField(upload_to='catalogs/', null=True, blank=True)

upload_to مشخص می‌کند فایل‌ها در کدام پوشه از media ذخیره شوند.

قدم دوم: تنظیمات media در settings.py

جنگو برای مدیریت فایل‌های آپلودی به دو تنظیم نیاز دارد:

# settings.py
import os

MEDIA_URL = '/media/'  # آدرس دسترسی به فایل‌ها در مرورگر
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')  # مسیر ذخیره فایل‌ها در دیسک

همچنین در urls.py اصلی، باید دسترسی به فایل‌های رسانه را در حالت توسعه فعال کنید:

# urls.py
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # مسیرهای شما
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

قدم سوم: سریالایزر با فیلد فایل

سریالایزر کاملاً معمولی است. فقط کافی است فیلد را اضافه کنید:

# serializers.py
class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'image', 'catalog']

نکته مهم: فیلدهای فایل read_only نیستند. کاربر می‌تواند در درخواست POST و PUT فایل بفرستد.

قدم چهارم: ویو برای آپلود فایل

ویو هم کاملاً معمولی است. DRF خودکار فایل را از request.FILES می‌خواند و به Serializer می‌دهد.

# views.py
from rest_framework.generics import CreateAPIView
from .models import Product
from .serializers import ProductSerializer

class ProductCreateView(CreateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

نکته مهم: کلاینت باید درخواست را با Content-Type: multipart/form-data بفرستد. نه application/json.

قدم پنجم: تست با Postman

در Postman:

  • متد POST را انتخاب کنید.
  • آدرس را وارد کنید.
  • برگه Body را انتخاب کنید.
  • گزینه form-data را انتخاب کنید.
  • هر فیلد متنی را با نوع Text اضافه کنید.
  • فیلد فایل را با نوع File اضافه کنید و فایل را انتخاب کنید.
  • درخواست بفرستید. فایل در پوشه media ذخیره می‌شود.

دریافت فایل (دانلود)

برای دسترسی به فایل آپلود شده، آدرس به این شکل است:

http://127.0.0.1:8000/media/products/myimage.jpg

برای برگرداندن آدرس کامل فایل در پاسخ API، می‌توانید از request.build_absolute_uri استفاده کنید:

class ProductSerializer(serializers.ModelSerializer):
    image_url = serializers.SerializerMethodField()
    
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'image_url']
    
    def get_image_url(self, obj):
        if obj.image:
            return self.context['request'].build_absolute_uri(obj.image.url)
        return None

اعتبارسنجی فایل

می‌توانید در Serializer اعتبارسنجی فایل را اضافه کنید

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'image']
    
    def validate_image(self, value):
        # بررسی حجم فایل (حداکثر ۵ مگابایت)
        if value.size > 5 * 1024 * 1024:
            raise serializers.ValidationError("حجم فایل نباید بیشتر از ۵ مگابایت باشد")
        
        # بررسی پسوند مجاز
        import os
        ext = os.path.splitext(value.name)[1].lower()
        if ext not in ['.jpg', '.jpeg', '.png', '.gif']:
            raise serializers.ValidationError("فقط پسوندهای jpg، png و gif مجاز هستند")
        
        return value

حذف فایل هنگام حذف مدل

وقتی یک مدل حذف می‌شود، فایل آن هنوز در دیسک می‌ماند. برای حذف خودکار فایل، می‌توانید سیگنال بنویسید یا از کتابخانه django-cleanup استفاده کنید:

pip install django-cleanup

سپس به INSTALLED_APPS اضافه کنید (آخرین اپ باشد):

INSTALLED_APPS = [
    ...
    'django_cleanup.apps.CleanupConfig',  # آخرین اپ
]

حالا هر وقت مدل حذف شود، فایل آن نیز خودکار پاک می‌شود.

نکات امنیتی مهم

  • هیچوقت به کاربران اجازه ندهید هر فایلی آپلود کنند. پسوند و حجم را محدود کنید.
  • نام فایل را به صورت تصادفی تغییر دهید. جنگو این کار را خودکار با upload_to انجام می‌دهد.
  • فایل‌های آپلودی را در پوشه‌ای خارج از static نگهداری کنید.
  • در محیط تولید، جنگو فایل‌های رسانه را سرو نمی‌کند. باید از Nginx یا Apache یا CDN استفاده کنید.

تمرین 

یک مدل UserProfile با فیلدهای user و avatar (ImageField) بسازید. یک API برای آپلود عکس پروفایل بنویسید. اعتبارسنجی کنید که فقط تصاویر زیر ۲ مگابایت قبول شوند.

جمع‌بندی دوره و مسیر یادگیری بعد از DRF

به آخرین بخش از این دوره رسیدیم. نه پایان یادگیری، بلکه پایان یک فصل و شروع فصل دیگر.

مرور مسیری که آمدید

از جایی شروع کردید که نمی‌دانستید API چیست و چرا به DRF نیاز دارید. حالا می‌دانید چطور یک API کامل بسازید. با سریالایزر، ویوهای مختلف، احراز هویت، مجوزها، فیلتر، جستجو، مرتب‌سازی، صفحه‌بندی، نسخه‌بندی، مستندسازی خودکار، و حتی آپلود فایل. مدیریت خطا را یاد گرفتید. محدودیت درخواست را بلدید. تفاوت روش‌های مختلف نوشتن ویو را می‌دانید و می‌توانید برای هر پروژه بهترین را انتخاب کنید.

این دانش برای ورود به بازار کار به عنوان توسعه‌دهنده بک‌اند با Django و DRF کافی است. اما توقف نکنید.

بعد از DRF چه بخوانیم؟

DRF فقط یک ابزار است. برای حرفه‌ای شدن در دنیای واقعی، باید دانش خود را در چند جهت دیگر هم گسترش دهید.

اول: دیتابیس پیشرفته

  • بهینه‌سازی کوئری‌ها (select_related, prefetch_related, indexing)
  • تراکنش‌های دیتابیس (atomic transactions)
  • آشنایی با دیتابیس‌های NoSQL مثل PostgreSQL (JSON field)

دوم: مفاهیم پیشرفته Django

  • Signals (سیگنال‌ها) برای رویدادهای سیستمی
  • Celery برای تسک‌های سنگین و زمان‌بر (ارسال ایمیل، پردازش عکس)
  • Caching با Redis یا Memcached برای افزایش سرعت

سوم: استقرار و DevOps

  • Docker و Docker Compose برای یکسان کردن محیط توسعه و تولید
  • Nginx و Gunicorn برای سرو کردن Django در تولید
  • آشنایی با CI/CD و گیت‌هاب اکشنز

چهارم: امنیت

  • آشنایی با حملات رایج (SQL Injection, XSS, CSRF)
  • استفاده از HTTPS و HSTS
  • مدیریت secrets و محیط‌های مختلف (توسعه، تست، تولید)

پنجم: معماری نرم‌افزار

  • تفکر در مقیاس (Scalability)
  • آشنایی با میکروسرویس‌ها (Microservices)
  • استفاده از message brokers مثل RabbitMQ یا Kafka

پروژه عملی پیشنهادی

بهترین راه برای تثبیت یادگیری، ساختن یک پروژه واقعی است. ایده پیشنهادی ما:

  • یک فروشگاه اینترنتی کامل با DRF
  • احراز هویت کاربران (ثبت‌نام، لاگین، پروفایل)
  • مدیریت محصولات (دسته‌بندی، جستجو، فیلتر، صفحه‌بندی)
  • سبد خرید و ثبت سفارش
  • پنل ادمین برای مدیریت سفارش‌ها
  • مستندات خودکار با drf-spectacular
  • محدودیت درخواست برای endpointهای حساس

این پروژه همه چیزهایی را که یاد گرفته‌اید، کنار هم می‌گذارد. بعد از اتمام، می‌توانید آن را در رزومه خود قرار دهید و در مصاحبه‌های شغلی درباره آن صحبت کنید.

یک توصیه پایانی

توسعه نرم‌افزار یک مسیر بی‌پایان است. همیشه چیز جدید برای یادگیری وجود دارد. اما مهم این است که همین الان با دانش فعلی خود می‌توانید ارزش ایجاد کنید.

ننشینید تا «کامل» شوید. بروید پروژه بزنید. اشتباه کنید. از اشتباه‌ها یاد بگیرید تا بهتر شوید.

این دوره تمام شد، اما یادگیری شما تازه شروع شده است.

موفق باشید.