Curated Skills
by lstudlo

cloudflare

references/kv/patterns.md

.md 197 lines
Content
# KV Patterns & Best Practices

## Multi-Tier Caching

```typescript
// Memory → KV → Origin (3-tier cache)
const memoryCache = new Map<string, { data: any; expires: number }>();

async function getCached(env: Env, key: string): Promise<any> {
  const now = Date.now();
  
  // L1: Memory cache (fastest)
  const cached = memoryCache.get(key);
  if (cached && cached.expires > now) {
    return cached.data;
  }
  
  // L2: KV cache (fast)
  const kvValue = await env.CACHE.get(key, "json");
  if (kvValue) {
    memoryCache.set(key, { data: kvValue, expires: now + 60000 }); // 1min in memory
    return kvValue;
  }
  
  // L3: Origin (slow)
  const origin = await fetch(`https://api.example.com/${key}`).then(r => r.json());
  
  // Backfill caches
  await env.CACHE.put(key, JSON.stringify(origin), { expirationTtl: 300 }); // 5min in KV
  memoryCache.set(key, { data: origin, expires: now + 60000 });
  
  return origin;
}
```

## API Response Caching

```typescript
async function getCachedData(env: Env, key: string, fetcher: () => Promise<any>): Promise<any> {
  const cached = await env.MY_KV.get(key, "json");
  if (cached) return cached;
  
  const data = await fetcher();
  await env.MY_KV.put(key, JSON.stringify(data), { expirationTtl: 300 });
  return data;
}

const apiData = await getCachedData(
  env,
  "cache:users",
  () => fetch("https://api.example.com/users").then(r => r.json())
);
```

## Session Management

```typescript
interface Session { userId: string; expiresAt: number; }

async function createSession(env: Env, userId: string): Promise<string> {
  const sessionId = crypto.randomUUID();
  const expiresAt = Date.now() + (24 * 60 * 60 * 1000);
  
  await env.SESSIONS.put(
    `session:${sessionId}`,
    JSON.stringify({ userId, expiresAt }),
    { expirationTtl: 86400, metadata: { createdAt: Date.now() } }
  );
  
  return sessionId;
}

async function getSession(env: Env, sessionId: string): Promise<Session | null> {
  const data = await env.SESSIONS.get<Session>(`session:${sessionId}`, "json");
  if (!data || data.expiresAt < Date.now()) return null;
  return data;
}
```

## Coalesce Cold Keys

```typescript
// ❌ BAD: Many individual keys
await env.KV.put("user:123:name", "John");
await env.KV.put("user:123:email", "john@example.com");

// ✅ GOOD: Single coalesced object
await env.USERS.put("user:123:profile", JSON.stringify({
  name: "John",
  email: "john@example.com",
  role: "admin"
}));

// Benefits: Hot key cache, single read, reduced operations
// Trade-off: Harder to update individual fields
```

## Prefix-Based Namespacing

```typescript
// Logical partitioning within single namespace
const PREFIXES = {
  users: "user:",
  sessions: "session:",
  cache: "cache:",
  features: "feature:"
} as const;

// Write with prefix
async function setUser(env: Env, id: string, data: any) {
  await env.KV.put(`${PREFIXES.users}${id}`, JSON.stringify(data));
}

// Read with prefix
async function getUser(env: Env, id: string) {
  return await env.KV.get(`${PREFIXES.users}${id}`, "json");
}

// List by prefix
async function listUserIds(env: Env): Promise<string[]> {
  const result = await env.KV.list({ prefix: PREFIXES.users });
  return result.keys.map(k => k.name.replace(PREFIXES.users, ""));
}

// Example hierarchy
"user:123:profile"
"user:123:settings"
"cache:api:users"
"session:abc-def"
"feature:flags:beta"
```

## Metadata Versioning

```typescript
interface VersionedData {
  version: number;
  data: any;
}

async function migrateIfNeeded(env: Env, key: string) {
  const result = await env.DATA.getWithMetadata(key, "json");
  
  if (!result.value) return null;
  
  const currentVersion = result.metadata?.version || 1;
  const targetVersion = 2;
  
  if (currentVersion < targetVersion) {
    // Migrate data format
    const migrated = migrate(result.value, currentVersion, targetVersion);
    
    // Store with new version
    await env.DATA.put(key, JSON.stringify(migrated), {
      metadata: { version: targetVersion, migratedAt: Date.now() }
    });
    
    return migrated;
  }
  
  return result.value;
}

function migrate(data: any, from: number, to: number): any {
  if (from === 1 && to === 2) {
    // V1 → V2: Rename field
    return { ...data, userName: data.name };
  }
  return data;
}
```

## Error Boundary Pattern

```typescript
// Resilient get with fallback
async function resilientGet<T>(
  env: Env,
  key: string,
  fallback: T
): Promise<T> {
  try {
    const value = await env.KV.get<T>(key, "json");
    return value ?? fallback;
  } catch (err) {
    console.error(`KV error for ${key}:`, err);
    return fallback;
  }
}

// Usage
const config = await resilientGet(env, "config:app", {
  theme: "light",
  maxItems: 10
});
```