📡 فصل ۹: ساخت API با Django REST Framework
برای اپ موبایل، SPA یا microservice نیاز به API داری؟ DRF بهترین انتخابه!
برای اپ موبایل، SPA یا microservice نیاز به API داری؟ DRF بهترین انتخابه!
📑 فهرست این فصل
🤔 REST API چیه؟
API راهی برای ارتباط بین برنامههاست. مثلاً اپ موبایل ICSD میخواد محصولات رو از سرور بگیره. بهجای اینکه HTML برگردونه، 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
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 ساده
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 (پرکاربردتر)
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)
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']
۴. فیلد محاسبهشده
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
۵. اعتبارسنجی
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 کلاسی
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)
۲. ویوهای ژنریک (سریعتر)
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ها
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ها رو خودکار میسازه!
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})
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
INSTALLED_APPS = [
# ...
'rest_framework.authtoken',
]
python manage.py migrate
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
# ...
path('api/login/', obtain_auth_token),
]
حالا کاربر با POST کردن نام کاربری و رمز، توکن میگیره و در درخواستهای بعدی استفاده میکنه:
GET /api/products/
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
JWT Authentication (مدرنتر)
pip install djangorestframework-simplejwt
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),
}
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view()),
path('api/token/refresh/', TokenRefreshView.as_view()),
]
🛡️ دسترسیها
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()]
دسترسی سفارشی
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 ساده برای لیست کتابها
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
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 نظرات با دسترسی
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
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
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 معتبر کار کنه.