~/icsd.ir — bash
SYSTEM_ONLINE

📡 فصل ۹: ساخت API با Django REST Framework

برای اپ موبایل، SPA یا microservice نیاز به API داری؟ DRF بهترین انتخابه!

برای اپ موبایل، SPA یا microservice نیاز به API داری؟ DRF بهترین انتخابه!

🤔 REST API چیه؟

API راهی برای ارتباط بین برنامه‌هاست. مثلاً اپ موبایل ICSD می‌خواد محصولات رو از سرور بگیره. به‌جای اینکه HTML برگردونه، JSON برمی‌گردونه:

پاسخ JSON
{
    "id": 1,
    "name": "فرش دستباف اصفهان",
    "price": "15000000.00",
    "stock": 5,
    "category": {
        "id": 2,
        "name": "فرش دستباف"
    }
}

روش‌های HTTP در REST

Method کاربرد مثال
GET دریافت لیست GET /api/products/
GET دریافت یک آیتم GET /api/products/1/
POST ساخت آیتم جدید POST /api/products/
PUT به‌روزرسانی کامل PUT /api/products/1/
PATCH به‌روزرسانی جزئی PATCH /api/products/1/
DELETE حذف DELETE /api/products/1/

📦 نصب و راه‌اندازی DRF

📟 ترمینال
pip install djangorestframework
myproject/settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
]

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

🔄 Serializer

Serializer داده‌ها رو بین فرمت‌های مختلف (JSON، Python) تبدیل می‌کنه. مثل Form برای API!

۱. Serializer ساده

products/serializers.py
from rest_framework import serializers

class ProductSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    name = serializers.CharField(max_length=200)
    price = serializers.DecimalField(max_digits=10, decimal_places=2)
    stock = serializers.IntegerField()
    
    def create(self, validated_data):
        return Product.objects.create(**validated_data)
    
    def update(self, instance, validated_data):
        instance.name = validated_data.get('name', instance.name)
        instance.price = validated_data.get('price', instance.price)
        instance.stock = validated_data.get('stock', instance.stock)
        instance.save()
        return instance

۲. ModelSerializer (پرکاربردتر)

products/serializers.py
from rest_framework import serializers
from .models import Product, Category

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', 'description', 'price', 'stock', 'category']
        # یا fields = '__all__'
        # یا exclude = ['created_at']
        
        read_only_fields = ['id', 'created_at']

۳. Serializer تو در تو (Nested)

products/serializers.py
class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name', 'slug']

class ProductSerializer(serializers.ModelSerializer):
    # نمایش کامل دسته به‌جای فقط ID
    category = CategorySerializer(read_only=True)
    
    # برای نوشتن (POST/PUT)
    category_id = serializers.PrimaryKeyRelatedField(
        queryset=Category.objects.all(),
        write_only=True,
        source='category'
    )
    
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'category', 'category_id']

۴. فیلد محاسبه‌شده

products/serializers.py
class ProductSerializer(serializers.ModelSerializer):
    is_in_stock = serializers.SerializerMethodField()
    image_url = serializers.SerializerMethodField()
    
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'is_in_stock', 'image_url']
    
    def get_is_in_stock(self, obj):
        return obj.stock > 0
    
    def get_image_url(self, obj):
        if obj.image:
            request = self.context.get('request')
            return request.build_absolute_uri(obj.image.url)
        return None

۵. اعتبارسنجی

products/serializers.py
class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['name', 'price', 'stock']
    
    def validate_price(self, value):
        if value <= 0:
            raise serializers.ValidationError('قیمت باید مثبت باشد')
        return value
    
    def validate_name(self, value):
        if Product.objects.filter(name=value).exists():
            raise serializers.ValidationError('این نام قبلاً ثبت شده')
        return value
    
    def validate(self, attrs):
        # اعتبارسنجی کلی
        if attrs['stock'] < 0:
            raise serializers.ValidationError({'stock': 'موجودی منفی نمی‌تواند باشد'})
        return attrs

🎯 APIView

۱. APIView کلاسی

products/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Product
from .serializers import ProductSerializer

class ProductListAPIView(APIView):
    def get(self, request):
        products = Product.objects.all()
        serializer = ProductSerializer(products, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = ProductSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class ProductDetailAPIView(APIView):
    def get_object(self, pk):
        try:
            return Product.objects.get(pk=pk)
        except Product.DoesNotExist:
            return None
    
    def get(self, request, pk):
        product = self.get_object(pk)
        if not product:
            return Response(status=status.HTTP_404_NOT_FOUND)
        serializer = ProductSerializer(product)
        return Response(serializer.data)
    
    def put(self, request, pk):
        product = self.get_object(pk)
        if not product:
            return Response(status=status.HTTP_404_NOT_FOUND)
        serializer = ProductSerializer(product, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    def delete(self, request, pk):
        product = self.get_object(pk)
        if not product:
            return Response(status=status.HTTP_404_NOT_FOUND)
        product.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

۲. ویوهای ژنریک (سریع‌تر)

products/views.py
from rest_framework import generics
from .models import Product
from .serializers import ProductSerializer

class ProductListCreateView(generics.ListCreateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

class ProductDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

این دو کلاس همه عملیات CRUD رو خودکار انجام می‌دن!

URLها

products/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('api/products/', views.ProductListCreateView.as_view()),
    path('api/products/<int:pk>/', views.ProductDetailView.as_view()),
]

🚀 ViewSet و Router

ViewSet چند ویو رو در یک کلاس ترکیب می‌کنه. Router هم URLها رو خودکار می‌سازه!

products/views.py
from rest_framework import viewsets, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Product
from .serializers import ProductSerializer

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ['name', 'description']
    ordering_fields = ['price', 'created_at']
    
    # اکشن سفارشی
    @action(detail=False, methods=['get'])
    def featured(self, request):
        # GET /api/products/featured/
        featured = Product.objects.filter(is_featured=True)
        serializer = self.get_serializer(featured, many=True)
        return Response(serializer.data)
    
    @action(detail=True, methods=['post'])
    def like(self, request, pk=None):
        # POST /api/products/1/like/
        product = self.get_object()
        product.likes += 1
        product.save()
        return Response({'likes': product.likes})
products/urls.py
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register('products', views.ProductViewSet)

urlpatterns = router.urls
# URL‌های ساخته‌شده:
# GET    /products/         → list
# POST   /products/         → create
# GET    /products/{id}/    → retrieve
# PUT    /products/{id}/    → update
# PATCH  /products/{id}/    → partial_update
# DELETE /products/{id}/    → destroy
# GET    /products/featured/ → اکشن سفارشی
# POST   /products/{id}/like/ → اکشن سفارشی

🔐 احراز هویت در API

Token Authentication

settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework.authtoken',
]
📟 ترمینال
python manage.py migrate
myproject/urls.py
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    # ...
    path('api/login/', obtain_auth_token),
]

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

درخواست HTTP
GET /api/products/
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

JWT Authentication (مدرن‌تر)

📟 ترمینال
pip install djangorestframework-simplejwt
settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

from datetime import timedelta
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}
urls.py
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view()),
    path('api/token/refresh/', TokenRefreshView.as_view()),
]

🛡️ دسترسی‌ها

products/views.py
from rest_framework.permissions import (
    IsAuthenticated,
    IsAuthenticatedOrReadOnly,
    IsAdminUser,
    AllowAny
)

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    
    # همه می‌تونن بخونن، فقط لاگین‌شده بنویسن
    permission_classes = [IsAuthenticatedOrReadOnly]
    
    # فقط ادمین
    # permission_classes = [IsAdminUser]
    
    # هرکس
    # permission_classes = [AllowAny]
    
    def get_permissions(self):
        # دسترسی متفاوت برای هر اکشن
        if self.action in ['create', 'update', 'destroy']:
            return [IsAdminUser()]
        return [AllowAny()]

دسترسی سفارشی

products/permissions.py
from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        # خواندن برای همه
        if request.method in permissions.SAFE_METHODS:
            return True
        # نوشتن فقط برای صاحب
        return obj.owner == request.user

🎯 مثال‌های کاربردی

آسان

مثال ۱: API ساده برای لیست کتاب‌ها

books/serializers.py
from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'
books/views.py
from rest_framework import generics
from .models import Book
from .serializers import BookSerializer

class BookList(generics.ListCreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

متوسط

مثال ۲: API نظرات با دسترسی

comments/views.py
from rest_framework import viewsets, permissions
from .models import Comment
from .serializers import CommentSerializer

class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.user == request.user

class CommentViewSet(viewsets.ModelViewSet):
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
    permission_classes = [
        permissions.IsAuthenticatedOrReadOnly,
        IsOwnerOrReadOnly
    ]
    
    def perform_create(self, serializer):
        # کاربر فعلی به‌عنوان مالک ست شه
        serializer.save(user=self.request.user)
    
    def get_queryset(self):
        # فیلتر بر اساس پارامتر URL
        product_id = self.request.query_params.get('product')
        if product_id:
            return Comment.objects.filter(product_id=product_id)
        return Comment.objects.all()

پیشرفته

مثال ۳: API کامل سفارش با چندین Serializer

orders/serializers.py
from rest_framework import serializers
from .models import Order, OrderItem
from products.models import Product

class OrderItemSerializer(serializers.ModelSerializer):
    product_name = serializers.CharField(source='product.name', read_only=True)
    
    class Meta:
        model = OrderItem
        fields = ['id', 'product', 'product_name', 'quantity', 'price']

class OrderListSerializer(serializers.ModelSerializer):
    """برای لیست ساده"""
    items_count = serializers.IntegerField(source='items.count', read_only=True)
    
    class Meta:
        model = Order
        fields = ['id', 'status', 'total_price', 'items_count', 'created_at']

class OrderDetailSerializer(serializers.ModelSerializer):
    """برای جزئیات کامل"""
    items = OrderItemSerializer(many=True, read_only=True)
    user_name = serializers.CharField(source='user.username', read_only=True)
    
    class Meta:
        model = Order
        fields = ['id', 'user', 'user_name', 'status', 'total_price', 
                  'items', 'address', 'created_at']
        read_only_fields = ['user', 'total_price']

class OrderCreateSerializer(serializers.ModelSerializer):
    """برای ساخت سفارش - شامل آیتم‌ها"""
    items = OrderItemSerializer(many=True)
    
    class Meta:
        model = Order
        fields = ['address', 'items']
    
    def create(self, validated_data):
        items_data = validated_data.pop('items')
        order = Order.objects.create(**validated_data)
        
        total = 0
        for item_data in items_data:
            item = OrderItem.objects.create(order=order, **item_data)
            total += item.price * item.quantity
        
        order.total_price = total
        order.save()
        return order
orders/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Order
from .serializers import (
    OrderListSerializer,
    OrderDetailSerializer,
    OrderCreateSerializer
)

class OrderViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated]
    
    def get_queryset(self):
        # هر کاربر فقط سفارشات خودش
        return Order.objects.filter(user=self.request.user)
    
    def get_serializer_class(self):
        # سرئالایزر متفاوت برای هر اکشن
        if self.action == 'list':
            return OrderListSerializer
        elif self.action == 'create':
            return OrderCreateSerializer
        return OrderDetailSerializer
    
    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

📝 تمرین‌ها

آسان

تمرین ۱

یه ModelSerializer برای مدل Tag بنویس و با ListCreateAPIView یه API ساده بساز.

متوسط

تمرین ۲

یه ViewSet برای مدل Post بساز که فیلتر، جستجو و صفحه‌بندی داشته باشه.

پیشرفته

تمرین ۳

یه API برای ثبت‌نام بساز که توکن JWT برگردونه. سپس یه ویو محافظت‌شده بساز که فقط با JWT معتبر کار کنه.

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.