Maqolalar 10 daqiqa 204 ko‘rish
Backend'ni mobil ilovaga moslash: versiyalash, payload va offline
Mobil mijoz — brauzer emas: eski versiyasi yillab yashaydi, tarmog'i uziladi, har ortiqcha so'rov batareya yeydi. API versiyalash, ekranga mos payload, ETag kesh, delta-sinxronizatsiya va uch tilni bitta javobda berish — Metro tajribasi.
Mundarija
Seriyaning avvalgi qismlarida Metro backend'ining bloklarini qurdik. Yakuniy qism — ularni birlashtiruvchi savol: mobil mijozga mos API qanday bo'ladi? Chunki mobil ilova brauzer emas, uning o'z qonunlari bor:
-
foydalanuvchi ilovani yangilamasligi mumkin — bir yil oldingi versiya bugun ham production'ga so'rov yuboradi;
-
tarmoq sekin va uzilib turadi — metroda ayniqsa;
- har ortiqcha so'rov — batareya va trafik;
- xato yuz bersa foydalanuvchi "qayta yuklash" tugmasini emas, ilovani o'chirib qo'yishni tanlaydi.
1-qoida: versiya URL'da, buzuvchi o'zgarish — yangi versiyada#
Saytda xato qildingiz — deploy qilasiz, bir daqiqada hammada yangi kod. Mobilda esa yangi versiya App Store tekshiruvidan o'tishi va foydalanuvchi uni o'rnatishi kerak. Siz maydon nomini o'zgartirgan kuni eski ilovalar sinadi — va ularni siz tuzata olmaysiz.
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
# versiya — yo'lning birinchi bo'g'ini. v1 hech qachon buzilmaydi:
# buzuvchi o'zgarish kerak bo'lsa, v2 ochiladi, v1 yashashda davom etadi
path("api/v1/", include("apps.api_v1.urls")),
]
apps/api_v1 — alohida model'lari yo'q, faqat v1 manzillarini bir joyga
yig'adigan paket (mkdir apps/api_v1 && touch apps/api_v1/__init__.py).
Oldingi qismlarda config/urls.py da turgan api/ qatorlari shu yerga
ko'chadi:
# apps/api_v1/urls.py
from django.urls import include, path
urlpatterns = [
path("", include("apps.metro.urls")),
path("", include("apps.notifications.urls")),
path("", include("apps.accounts.urls")),
]
Mobil ilovadagi manzillar ham /api/... dan /api/v1/... ga o'zgaradi —
buni birinchi reliz'dan oldin qilish kerak, keyin kech bo'ladi. Testlardagi
manzillarni ham yangilang: masalan, 4-qism testlaridagi /api/auth/google/
endi /api/v1/auth/google/, aks holda ular 404 oladi.
Qoidalar sodda:
-
qo'shish mumkin: yangi maydon, yangi endpoint — eski mijozlar ularni ko'rmaydi, buzilmaydi;
-
mumkin emas: maydonni o'chirish, nomini yoki tipini o'zgartirish, majburiy parametr qo'shish — bularning har biri yangi versiya degani;
-
eski versiyani o'chirishdan oldin uning trafigini o'lchang (access log yoki metrika) — nol bo'lmaguncha o'chirmang.
Nega header emas, URL
Versiyani Accept header'da berish "akademik to'g'ri" hisoblanadi, lekin
URL versiyasi log'da ko'rinadi, curl bilan tekshiriladi, mobil jamoaga
tushuntirish oson. Amaliyotda soddalik yutadi — Metro'da ham /api/v1/.
2-qoida: bitta ekran — bitta so'rov#
Web'da sahifa 5 ta so'rov qilsa sezilmaydi. Mobilda har so'rov — alohida TLS-qo'l berish, radiouyg'onish, kechikish. Metro'ning bosh ekrani liniyalar va stansiyalarni ko'rsatadi — ularni ikki endpoint qilib bermaymiz, ekranga mos bitta javob qilamiz:
# apps/metro/serializers.py
from rest_framework import serializers
from .models import Line, Station
class StationNestedSerializer(serializers.ModelSerializer):
class Meta:
model = Station
fields = ["id", "name", "status"]
class LineWithStationsSerializer(serializers.ModelSerializer):
stations = StationNestedSerializer(many=True, read_only=True)
class Meta:
model = Line
fields = ["id", "name", "color", "stations"]
# apps/metro/views.py
from rest_framework.generics import ListAPIView
from .models import Line
from .serializers import LineWithStationsSerializer
class NetworkView(ListAPIView):
"""Bosh ekran uchun butun tarmoq: liniyalar + ichida stansiyalari."""
serializer_class = LineWithStationsSerializer
# prefetch bo'lmasa har liniya uchun alohida so'rov ketadi — N+1
queryset = Line.objects.prefetch_related("stations").order_by("id")
# apps/metro/urls.py — 1-qismdagi ro'yxatga
path("network/", NetworkView.as_view()),
Ichma-ich serializer N+1 muammosining klassik manbai — prefetch_related
esdan chiqsa, 4 liniya 5 ta SQL so'rov bo'ladi. Bu tuzoqni alohida yoritganman:
Django ORM va N+1.
3-qoida: o'zgarmagan javobni qayta yubormang — ETag#
Metro tarmog'i haftalab o'zgarmaydi, lekin ilova har ochilganda so'raydi.
ConditionalGetMiddleware bilan Django javobga ETag qo'shadi va mijoz
If-None-Match yuborsa, o'zgarmagan javobga 304 (bo'sh tana) qaytaradi.
Javoblarni siqish uchun yoniga GZipMiddleware qo'yamiz — lekin ikkalasining
joyi muhim:
# config/settings.py
MIDDLEWARE = [
# ro'yxat BOSHIDA: javob tanasini hamma tayyorlab bo'lgach, eng oxirida siqadi
"django.middleware.gzip.GZipMiddleware",
"django.middleware.security.SecurityMiddleware",
# ...
"django.middleware.http.ConditionalGetMiddleware", # ETag/304 avtomatik
]
Django javobni middleware ro'yxati bo'ylab pastdan yuqoriga qaytaradi.
GZipMiddleware pastda tursa, siqish ETag hisoblanishidan oldin bo'ladi.
Django 4.2 dan beri esa gzip BREACH hujumiga qarshi har javobga tasodifiy
baytlar qo'shadi — siqilgan tana har safar boshqacha, demak ETag ham har
so'rovda yangi va 304 hech qachon qaytmaydi. Sinovda aynan shunday bo'ldi:
teskari tartibda gzip so'ragan mijoz ikki ketma-ket so'rovda ikki xil ETag
oldi va If-None-Match ga 200 qaytdi. Bu xato gzip so'ramaydigan oddiy
curl da ko'rinmaydi, mobil kutubxonalar esa gzip'ni doim so'raydi.
To'g'ri tartibda ETag siqilmagan tanadan hisoblanadi, gzip uni "zaif"
(W/) deb belgilaydi va 304 ishlaydi.
# birinchi so'rov: mobil mijozdek gzip so'raymiz — to'liq javob + ETag
curl -s -o /dev/null -D - -H "Accept-Encoding: gzip" \
https://api.misol.uz/api/v1/network/ | grep -i etag
# ETag: W/"2de59cffffc80ec7293019e9a02c8b1e"
# ikkinchi so'rov: mijoz ETag'ni qaytaradi — javob 304, tana bo'sh
curl -s -o /dev/null -D - -H "Accept-Encoding: gzip" \
-H 'If-None-Match: W/"2de59cffffc80ec7293019e9a02c8b1e"' \
https://api.misol.uz/api/v1/network/ | head -1
# HTTP/1.1 304 Not Modified
Bu ikki qatorlik middleware metro kabi "o'qish ko'p, yozish kam" API'da trafikning katta qismini yo'q qiladi. Server tomonda hisoblash baribir ketadi — uni ham kesish kerak bo'lsa, keyingi bosqich kesh: Redis keshlash strategiyalari.
4-qoida: offline — xato holati emas, normal holat#
Metro ilovasining qiziq sharti: foydalanuvchi aynan yer ostida, aloqasiz paytda ilovaga eng ko'p qaraydi. Yechim — ma'lumotni qurilmada saqlash va tarmoq borida delta-sinxronizatsiya:
# apps/metro/views.py
from django.utils.dateparse import parse_datetime
from rest_framework.exceptions import ValidationError
from rest_framework.generics import ListAPIView
from .models import Station
from .serializers import StationSyncSerializer
class StationSyncView(ListAPIView):
"""?updated_after=2026-08-01T00:00:00Z — faqat o'shandan keyin o'zgarganlar.
Ilova birinchi ochilishda hammasini oladi va lokal bazaga yozadi,
keyin faqat farqni so'raydi — trafik ham, vaqt ham minimal.
"""
serializer_class = StationSyncSerializer
def get_queryset(self):
qs = Station.objects.order_by("updated_at")
raw = self.request.query_params.get("updated_after")
if not raw:
return qs
moment = parse_datetime(raw)
if moment is None:
raise ValidationError({"updated_after": "ISO 8601 formatida bo'lsin"})
return qs.filter(updated_at__gt=moment)
# apps/metro/urls.py — 1-qismdagi ro'yxatga
path("stations/sync/", StationSyncView.as_view()),
Buning ishlashi uchun modelda updated_at = models.DateTimeField(auto_now=True)
bo'lishi kerak — va o'chirilgan yozuvlar uchun qattiq o'chirish o'rniga
is_deleted bayrog'i (aks holda mijoz o'chirilganni bilmay qoladi).
StationSyncSerializer — oddiy ModelSerializer, fields ichida id,
updated_at va is_deleted albatta bo'lsin.
Mijoz tomonda ikki nozik joy bor:
-
updated_afterga telefon soatini emas, oldingi javobda kelgan eng kattaupdated_atni yuboring. Telefon soati bir necha daqiqa oldinda bo'lsa, shu daqiqalarda o'zgargan stansiyalar hech qachon yetib kelmaydi; -
vaqtni URL'ga kodlab qo'ying.
2026-08-01T05:00:00+05:00dagi+query satrida bo'sh joyga aylanadi va endpoint to'g'ri sanaga ham400qaytaradi.Zbilan tugaydigan UTC vaqt yoki%2Bbilan kodlangan+muammosiz o'tadi.
Takroriy so'rovga chidamlilik (idempotentlik)
Mobil tarmoqda so'rov ketdi-yu, javob yo'qoldi — ilova qayta uradi. POST
endpoint bunga tayyor bo'lmasa, bitta amal ikki marta bajariladi.
Retseptlar: yaratishda update_or_create (3-qismdagi token ro'yxati shunday
ishlaydi), pul/kritik amallarda mijoz yuboradigan Idempotency-Key header
va uni server tomonda eslab qolish.
5-qoida: uch til — bitta javobda#
Metro kontenti uch tilda (uz/ru/en). Klassik yechim — Accept-Language bo'yicha
bitta tilni qaytarish. Metro'da esa boshqa qaror qabul qilingan: hamma til
bitta JSONField'da, javob ham shunday ketadi:
{"id": 4, "name": {"uz": "Amir Temur xiyoboni", "ru": "Сквер Амира Темура", "en": "Amir Temur Square"}}
Sabablari amaliy:
-
foydalanuvchi tilni ilova ichida, offline holda almashtira oladi — server so'rovsiz;
-
kesh va ETag samaradorligi: javob tilga bog'liq emas, bitta variant;
- uch qisqa satr javob hajmini sezilarli oshirmaydi. Kontent hajmi cheklangan ma'lumotnoma bo'lgani uchun bu qaror o'zini oqlaydi; uzun matnlar bilan ishlaydigan tizimda esa boshqacha yo'l tanlangan bo'lardi.
Xato javoblari ham API'ning qismi
Ilova xatoni foydalanuvchi tilida ko'rsatishi kerak. Matn yuborish o'rniga
kod yuboring — {"error": {"code": "station_closed"}} — tarjimasi
ilovada. Matn yuborsangiz, xabarni o'zgartirish uchun yana App Store
navbatiga turasiz.
Xulosa — mobil API dizaynining besh sinovi#
Har yangi endpoint'ni shu savollar bilan tekshiring:
- Bir yil oldingi ilova versiyasi bu javobni ko'rsa sinadimi? (versiyalash)
- Ekran nechta so'rov qilyapti? (payload dizayni, N+1)
- O'zgarmagan ma'lumot qayta yuborilyaptimi? (ETag/304, kesh)
- So'rov ikki marta kelsa nima bo'ladi? (idempotentlik)
- Javob aloqasiz metro vagonida ham ma'noli ishlaydimi? (delta-sync, offline)
Shu bilan Metro backend amaliyoti seriyasi yakunlandi: geo-so'rovlar, real-time, push, autentifikatsiya va mobil dizayn — birgalikda Play Market'da 100K+ yuklab olingan ilovaning to'liq backend skeleti. Loyihaning o'zi haqida: Tashkent Metro case-study.