· 5 dk okuma
Sunucusuz Bir Web Uygulamasında Dış API'lerle Güvenli Çalışmak
Bir web uygulamasının kendi sunucusu olmadığında tarayıcı, dış servislerle doğrudan konuşur. Bu mimari basittir ve ucuzdur, ama güvenlik sorumluluğunun tamamını tarayıcıda çalışan koda bırakır: servisleri seçmek, gelen veriyi doğrulamak, tarayıcının nereye bağlanabileceğini sınırlamak. Bu yazıda hava durumu uygulamam Weather App'te bu üç işi nasıl yaptığımı ve bu mimarinin bedelini anlatıyorum.
Mimari: tarayıcı ve üç servis
Weather App sade HTML, CSS ve JavaScript ile yazılmış, derleme adımı ve npm bağımlılığı olmayan bir uygulama. Üç şeye ihtiyacı var: hava tahmini, şehir araması ve konum izni verilince bulunulan yerin adı. Bunlar için üç servis kullanıyor:
| İhtiyaç | Servis |
|---|---|
| Anlık hava, 24 saatlik ve 5 günlük tahmin, dünya şehirleri araması | Open-Meteo |
| Türkiye'deki il, ilçe ve mahalle araması | Photon (komoot, OpenStreetMap verisi) |
| Koordinattan yer adı | BigDataCloud |
Bir kullanıcı "Ankara" yazdığında akış şöyle:
- Tarayıcı aramayı Open-Meteo ve Photon'a aynı anda gönderir.
- Gelen sonuçlar doğrulanır, birleştirilir ve listelenir.
- Kullanıcı bir şehir seçince tarayıcı Open-Meteo'dan tahmini ister.
- Yanıt doğrulanır, sade bir nesneye çevrilir ve ekrana yazılır.
Arada benim sunucum yok. Sunucu tarafında sadece küçük bir sayfa fonksiyonu var; görevi /istanbul ya da /londra gibi her şehir adresine arama motorları için kendi başlığını ve açıklamasını yazmak. Hava verisine dokunmuyor, dışarıya istek atmıyor.
Karar 1: tarayıcıda kullanılabilecek servisler seçmek
Sunucu yoksa, tarayıcıya giden kodda saklanması gereken hiçbir şey olmamalı. Tarayıcıda çalışan koddaki her değer, sayfanın kaynağını ya da geliştirici araçlarının ağ sekmesini açan herkes tarafından görülebilir. Bir değeri .env dosyasına koymak da bunu değiştirmez: derleme aracı o değeri yine tarayıcıya giden koda yazar.
Bu yüzden servisleri seçerken ilk ölçüt, tarayıcıdan doğrudan ve gizli bir bilgi olmadan kullanılabilmeleriydi. Üçü de kayıt ya da gizli değer istemeden, tarayıcıdan çağrılmaya izin veren açık servisler:
// Open-Meteo istekleri. Anahtar gerekmez; yanıtlar weather.js ve places.js ile doğrulanır.
const FORECAST_URL = 'https://api.open-meteo.com/v1/forecast';
const GEOCODING_URL = 'https://geocoding-api.open-meteo.com/v1/search';
const PHOTON_URL = 'https://photon.komoot.io/api/';
const REVERSE_URL = 'https://api.bigdatacloud.net/data/reverse-geocode-client';
İhtiyaç duyulan servis gizli bir değer gerektirseydi bu mimari uygun olmazdı. O durumda istekleri bir sunucudan geçirmek, sunucuda istek sınırlama ve girdi doğrulama yapmak gerekirdi. Weather App'in ihtiyaçları açık servislerle karşılanabildiği için sunucusuz mimariyi seçtim.
Karar 2: dışarıdan gelen hiçbir veriye güvenmemek
Tarayıcı üç dış servisten veri alıyor ve bu veri doğrudan ekrana gidiyor. Servislerden biri bozuk, eksik ya da beklenmedik bir yanıt döndürürse uygulama çökmemeli, ekrana tuhaf bir şey yazmamalı.
Bu yüzden hiçbir yanıt doğrudan kullanılmıyor. Önce sade bir nesneye çevriliyor ve her alan kontrol ediliyor. Sayı beklenen her yerde şu küçük yardımcı var:
const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
Anlık hava yanıtı şöyle okunuyor:
export function parseForecast(raw) {
const current = raw?.current;
if (typeof current?.time !== 'string' || num(current.temperature_2m) === null) return null;
return {
current: {
time: current.time,
temp: current.temperature_2m,
feels: num(current.apparent_temperature),
humidity: num(current.relative_humidity_2m),
wind: num(current.wind_speed_10m),
code: num(current.weather_code),
isDay: current.is_day !== 0,
},
// ...
};
}
Kurallar basit:
- Sıcaklık yoksa yanıt hiç kullanılmıyor ve ekranda anlaşılır bir hata görünüyor.
- Nem ya da rüzgâr eksikse o alan "—" oluyor.
- Bozuk bir saat satırı atlanıyor, bilinmeyen alanlar hiç alınmıyor.
- Saat dilimi bile kontrol ediliyor:
<script>gibi bir değer gelirse kabul edilmiyor. Bu durum bir testle de sınanıyor.
Arama sonuçları da aynı şekilde temizleniyor. Bir sonuç ancak adı, geçerli bir enlemi (±90) ve boylamı (±180) varsa listeye giriyor. Metinler 100 karakterle sınırlanıyor ve sadece bilinen alanlar saklanıyor. Kullanıcının kaydettiği şehirler tarayıcının yerel deposundan okunurken de aynı kontrolden geçiyor, çünkü yerel depo elle değiştirilebilir.
Ekrana yazarken de dikkat gerekiyor. Uygulamada hiç innerHTML yok: metinler textContent ile yazılıyor, hava simgeleri ve grafik SVG öğeleri tek tek oluşturularak çiziliyor. Bir servis şehir adı yerine HTML kodu döndürse bile bu kod çalışmaz, düz yazı olarak görünür.
Karar 3: tarayıcıyı Content Security Policy ile sınırlamak
Sunucu olmayınca güvenlik kurallarını tarayıcıya HTTP başlıklarıyla anlatmak gerekiyor. En önemlisi Content Security Policy (CSP). Weather App'te bağlantı izni verilen adresler tek tek sayılı:
connect-src 'self'
https://api.open-meteo.com
https://geocoding-api.open-meteo.com
https://photon.komoot.io
https://api.bigdatacloud.net
...
Listede olmayan bir adrese sayfadan istek atılamıyor. script-src sadece sitenin kendi dosyalarına ve bir analitik betiğine izin veriyor; satır içi betik ve eval yok. Konum izni de bir başlıkla sınırlanmış: geolocation=(self) sadece bu sitenin konum istemesine izin veriyor, sayfaya gömülen başka bir içerik isteyemez.
Yerel geliştirme sunucusu bu başlıkların aynısını gönderiyor. Böylece CSP'nin engelleyeceği bir istek canlıya çıkmadan, geliştirirken fark ediliyor.
Konum: sadece gerektiği kadar
"Konumumu kullan" düğmesine basılınca tarayıcı izin istiyor. İzin verilirse koordinatlar iki yere gidiyor: tahmin için Open-Meteo'ya, yer adı için BigDataCloud'a. Koordinatlar dört ondalık basamakla gönderiliyor; bu yaklaşık 11 metrelik bir hassasiyet ve hava durumu için fazlasıyla yeterli. Konum hiçbir yere kaydedilmiyor. İzin verilmezse kısa bir uyarı çıkıyor ve arama çalışmaya devam ediyor.
Mimarinin bedeli
Bu mimarinin de bir bedeli var:
- Bağımlılık: Uygulama üç ücretsiz servise bağlı. Bir servis kapanırsa ya da kurallarını değiştirirse yerine yenisini koymak gerekir.
- Kullanım sınırları: Ücretsiz servislerin kullanım sınırları ve koşulları var; bunlara uymak benim sorumluluğum.
- Kontrol: Araya bir sunucu koysaydım önbellek ekleyebilir, servisler arasında görünmeden geçiş yapabilirdim. Şimdi bunların tarayıcıda yapılması gerekiyor.
Bunları hafifletmek için iki şey yaptım. Her isteğin 8 saniyelik bir zaman aşımı var; cevap gelmezse sayfa sonsuza kadar beklemiyor:
async function getJson(url) {
const response = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
Şehir araması da iki kaynağa aynı anda gidiyor. Biri çalışmazsa diğerinin sonuçları yine gösteriliyor; ikisi de çalışmazsa "Arama yapılamadı. Bağlantını kontrol edip tekrar dene." mesajı çıkıyor.
Ne zaman bir sunucu gerekir?
Sunucusuz mimari her uygulamaya uymaz. Şu durumlarda istekleri bir sunucudan geçirmek gerekir:
- Kullanılacak servis gizli bir değerle çağrılıyorsa.
- Servis ücretliyse; o zaman isteklerin uygulama sahibinin kontrolünde olması gerekir.
- Kullanıcıya özel veriler, ödeme ya da hesap işlemleri varsa.
Weather App'te bunların hiçbiri yoktu. Bu yüzden tarayıcının servislerle doğrudan konuştuğu bir mimari yeterliydi; güvenlik de bu mimariye göre kuruldu.
Özet
- Sunucusuz bir uygulamada tarayıcıya giden koddaki her şey herkese açıktır; servisler de buna göre, tarayıcıdan gizli bir bilgi olmadan kullanılabilenlerden seçildi.
- Dış servislerden gelen her yanıt doğrulanıyor, eksik ya da bozuk alanlar ekrana ulaşmıyor.
- Ekrana sadece
textContentile yazılıyor; uygulamadainnerHTMLyok. - CSP tarayıcının bağlanabileceği adresleri sınırlıyor, konum izni sadece bu siteye açık.
- Zaman aşımı ve ikinci arama kaynağı, dış servislere bağımlılığın etkisini azaltıyor.
Weather App canlı olarak weather.miracdeprem.com adresinde. Kaynak kodu GitHub'da: github.com/MrcDprm/weather-app.