وقتی مشتری شما یک اپ موبایل است
اپ اندرویدی فروشندگان، داشبورد React یا نرمافزار حسابداری که باید سفارشها را بخواند، HTML نمیخواهند؛ JSON میخواهند. Django REST Framework (DRF) استاندارد عملی ساخت API در جنگوست: سریالسازی، اعتبارسنجی، احراز هویت، مجوز، صفحهبندی و مستندات قابلمرور، همه آماده.
pip install djangorestframework
# settings.py
INSTALLED_APPS += ["rest_framework"]
Serializer: پل بین مدل و JSON
Serializer همان نقشی را در API دارد که Form در HTML: دادهی ورودی را اعتبارسنجی و به شیء تبدیل میکند و شیء را برای خروجی به dict تبدیل میکند.
# apps/api/serializers.py
from rest_framework import serializers
from apps.catalog.models import Carpet
from apps.orders.models import Order, OrderItem
class CarpetSerializer(serializers.ModelSerializer):
price_toman = serializers.SerializerMethodField()
density_label = serializers.CharField(source="get_density_display", read_only=True)
class Meta:
model = Carpet
fields = ["id", "code", "name", "density", "density_label", "price", "price_toman", "stock"]
read_only_fields = ["stock"]
def get_price_toman(self, obj):
return obj.price // 10
def validate_price(self, value):
if value % 10_000:
raise serializers.ValidationError("قیمت باید مضرب ده هزار ریال باشد.")
return value
class OrderItemSerializer(serializers.ModelSerializer):
carpet = CarpetSerializer(read_only=True)
class Meta:
model = OrderItem
fields = ["carpet", "quantity", "unit_price"]
class OrderSerializer(serializers.ModelSerializer):
items = OrderItemSerializer(many=True, read_only=True)
customer_name = serializers.CharField(source="customer.full_name", read_only=True)
class Meta:
model = Order
fields = ["id", "tracking_code", "status", "total", "customer_name", "items", "created_at"]
ViewSet و Router
# apps/api/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
class CarpetViewSet(viewsets.ModelViewSet):
queryset = Carpet.objects.filter(status="active").order_by("code")
serializer_class = CarpetSerializer
lookup_field = "code"
@action(detail=True, methods=["get"])
def stock(self, request, code=None):
carpet = self.get_object()
return Response({"code": carpet.code, "stock": carpet.stock})
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
def get_queryset(self):
return (Order.objects.for_user(self.request.user)
.select_related("customer").prefetch_related("items__carpet"))
# apps/api/urls.py
from django.urls import include, path
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("carpets", CarpetViewSet)
router.register("orders", OrderViewSet, basename="order")
urlpatterns = [path("v1/", include(router.urls))]
همین چند خط این endpointها را میسازد: GET/POST /api/v1/carpets/، GET/PUT/PATCH/DELETE /api/v1/carpets/{code}/، GET /api/v1/carpets/{code}/stock/ و فهرست و جزئیات سفارشهای خود کاربر.
نکتههایی که کمتر کسی میداند
- وقتی ViewSet بهجای ویژگی
querysetمتدget_querysetدارد،basenameدر router الزامی است؛ وگرنه خطای «basename argument not specified» میگیرید. - ModelSerializer متد
clean()مدل را صدا نمیزند؛ قواعد مدل را درvalidate()سریالایزر تکرار کنید یا در آنجاinstance.full_clean()را صدا بزنید. - Serializerهای تودرتو N+1 میسازند؛ همیشه در
get_querysetViewSet، select_related و prefetch_related متناسب با فیلدهای سریالایزر بگذارید. SerializerMethodFieldفقطخواندنی است؛ برای فیلد محاسبهشدهی قابلنوشتن ازsourceیا بازنویسیto_internal_valueاستفاده کنید.- در سرور میتوانید Browsable API را با حذف
BrowsableAPIRendererازDEFAULT_RENDERER_CLASSESخاموش کنید؛ هم سطح حمله کمتر میشود هم ساختار داخلی لو نمیرود.