Mobil ve web'i aynı takvim mantığıyla beslemek (Looplio günlüğü)
Looplio'da tek bir iş mantığını hem React Native hem web istemcisine tutarlı biçimde sunmanın pratik kararları ve öğretileri.
Looplio’yu tek başıma geliştirirken en çok dikkat gerektiren sorun şu oldu: aynı takvim mantığını hem mobil hem web istemcisine sunmak. Kulağa basit geliyor — “API yaz, her iki taraf tüketsin.” Ama pratikte, iki istemcinin aynı verideki beklentisi, sunuş biçimi ve zamansal davranışı farklılaşıyor. Bu farkı erkenden görememek, ilerleyen haftalarda çifte bakım yükü yaratıyor.
Nisan ayında bu dersi biraz geç çıkardım.
Tek API, farklı beklentiler
Looplio’nun özünde bir recurrence motoru var. Kullanıcı “her Pazartesi” ya da “ayın ilk günü” gibi bir kural tanımlıyor; API bu kuralı belirli bir zaman aralığına açarak somut tarihler üretiyor.
Web istemcisi bu veriyi aylık bir grid görünümünde kullanıyor: o ay içindeki tüm olaylar tek seferde geliyor ve bir takvim tabloya işleniyor. Mobil ise farklı çalışıyor: kullanıcı parmağını aşağı çekiyor, sonraki olaylar akışa dahil oluyor — bir tür paginated zaman akışı.
İlk hatam şuydu: iki istemci için ayrı endpoint’ler yazmaya başlamak. /api/events/monthly ve /api/events/upcoming gibi. Bu yol kısa vadede hız kazandırıyor; ancak aynı mantığı iki yerde tutmak demek. İş kuralı değiştiğinde iki kez güncellemeniz gerekiyor.
Parametreli tek uç nokta
Çözüm aslında basit: tek bir uç nokta, farklı parametrelerle çağrılıyor.
GET /api/events?from=2024-04-01&to=2024-04-30
GET /api/events?from=2024-04-07&limit=20
Uç nokta her iki durumda da aynı sorgu motorunu kullanıyor; yalnızca hangi tarihleri kapsayacağı ve kaç kayıt döneceği değişiyor. Yeni bir filtreleme kuralı eklendiğinde, iki dosyayı değil birini güncelliyorum.
Laravel’de bu tür esnek sorgular için bir query builder sınıfı yazmak işe yarıyor:
class EventQueryBuilder
{
public function __construct(
private readonly User $user,
private readonly EventRepository $repo
) {}
public function forRange(CarbonInterface $from, CarbonInterface $to): Collection
{
return $this->repo->expandRecurrences($this->user, $from, $to);
}
public function upcoming(CarbonInterface $after, int $limit = 20): Collection
{
return $this->repo->expandRecurrences($this->user, $after, $after->copy()->addMonths(3))
->filter(fn ($e) => $e->date->gt($after))
->take($limit)
->values();
}
}
Controller bu sınıfı alıyor ve parametrelere göre doğru metodu çağırıyor. İş mantığı katmanın içinde; controller yalnızca koordinasyon yapıyor.
Resource biçimlendirmesi
İki istemci bazen aynı verinin farklı alanlarına ihtiyaç duyuyor. Web tüm meta veriyi istiyor; mobil uygulama hız için daha sade bir yanıt tercih ediyor.
Bunun için API resource sınıfı içinde koşullu alanlar kullanıyorum:
class EventResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'date' => $this->date->toIso8601String(),
'color' => $this->color,
$this->mergeWhen($request->is('*/web/*'), [
'recurrence_rule' => $this->recurrence_rule,
'notes' => $this->notes,
'created_at' => $this->created_at->toIso8601String(),
]),
];
}
}
URL’deki /web/ ya da /mobile/ segmentini kontrol etmek yerine istek başlığı ya da query parametresi de kullanılabilir. Önemli olan: resource sınıfı bunu yönetiyor, istemciler farklı payload alıyor ama iş mantığı tek kalıyor.
Tarih ve saat dilimi meselesi
Takvim uygulamalarının klasik tuzağı: veritabanında UTC sakladığınız zaman, istemciye hangi saat diliminde döneceğinize karar vermeniz gerekiyor.
Kararım: sunucu her zaman UTC döner; istemciler kendi saat diliminde gösterir. Mobil tarafta React Native’de Intl.DateTimeFormat veya date-fns-tz bu dönüşümü yapıyor. Web tarafında da aynı.
Bu karar API’yi saat dilimi kararından özgür bırakıyor. Kullanıcı saat dilimini değiştirirse, API yanıtı değişmez; istemci yeniden hesaplar. Tutarlı ve öngörülebilir.
Öğrenilen şey
İki istemci için ayrı ayrı yazmak başlangıçta hızlı hissettiriyor. Ama birkaç özellik ekledikten sonra bakım yükü katlanıyor. Bir karar değiştiğinde iki yerde aramak, iki yerde test etmek zorundayım.
Bir API yazıyorsanız ve birden fazla istemci varsa, en değerli iş şu soruyu sormak: “Bu iki istemci gerçekten farklı bir iş kuralı mı istiyor, yoksa aynı verinin farklı bir sunum biçimini mi?” Çoğu zaman ikincisi. Ve o zaman cevap: tek bir iş mantığı, parametreli bir interface.