Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:21 +08:00
commit a5d916d454
11 changed files with 3951 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "cloudflare-kv",
"description": "Store key-value data globally with Cloudflare KVs edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"skills": [
"./"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# cloudflare-kv
Store key-value data globally with Cloudflare KVs edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.

429
SKILL.md Normal file
View File

@@ -0,0 +1,429 @@
---
name: cloudflare-kv
description: |
Store key-value data globally with Cloudflare KV's edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.
license: MIT
---
# Cloudflare Workers KV
**Status**: Production Ready ✅
**Last Updated**: 2025-11-24
**Dependencies**: cloudflare-worker-base (for Worker setup)
**Latest Versions**: wrangler@4.50.0, @cloudflare/workers-types@4.20251121.0
**Recent Updates (2025)**:
- **August 2025**: Architecture redesign (40x performance gain, <5ms p99 latency, hybrid storage with R2)
- **January 2025**: Namespace limit increased (200 → 1,000 namespaces per account for Free and Paid plans)
---
## Quick Start (5 Minutes)
```bash
# Create namespace
npx wrangler kv namespace create MY_NAMESPACE
# Output: [[kv_namespaces]] binding = "MY_NAMESPACE" id = "<UUID>"
```
**wrangler.jsonc:**
```jsonc
{
"kv_namespaces": [{
"binding": "MY_NAMESPACE", // Access as env.MY_NAMESPACE
"id": "<production-uuid>",
"preview_id": "<preview-uuid>" // Optional: local dev
}]
}
```
**Basic Usage:**
```typescript
type Bindings = { MY_NAMESPACE: KVNamespace };
app.post('/set/:key', async (c) => {
await c.env.MY_NAMESPACE.put(c.req.param('key'), await c.req.text());
return c.json({ success: true });
});
app.get('/get/:key', async (c) => {
const value = await c.env.MY_NAMESPACE.get(c.req.param('key'));
return value ? c.json({ value }) : c.json({ error: 'Not found' }, 404);
});
```
---
## KV API Reference
### Read Operations
```typescript
// Get single key
const value = await env.MY_KV.get('key'); // string | null
const data = await env.MY_KV.get('key', { type: 'json' }); // object | null
const buffer = await env.MY_KV.get('key', { type: 'arrayBuffer' });
const stream = await env.MY_KV.get('key', { type: 'stream' });
// Get with cache (minimum 60s)
const value = await env.MY_KV.get('key', { cacheTtl: 300 }); // 5 min edge cache
// Bulk read (counts as 1 operation)
const values = await env.MY_KV.get(['key1', 'key2']); // Map<string, string | null>
// With metadata
const { value, metadata } = await env.MY_KV.getWithMetadata('key');
const result = await env.MY_KV.getWithMetadata(['key1', 'key2']); // Bulk with metadata
```
### Write Operations
```typescript
// Basic write (max 1/second per key)
await env.MY_KV.put('key', 'value');
await env.MY_KV.put('user:123', JSON.stringify({ name: 'John' }));
// With expiration
await env.MY_KV.put('session', data, { expirationTtl: 3600 }); // 1 hour
await env.MY_KV.put('token', value, { expiration: Math.floor(Date.now()/1000) + 86400 });
// With metadata (max 1024 bytes)
await env.MY_KV.put('config', 'dark', {
metadata: { updatedAt: Date.now(), version: 2 }
});
```
**Critical Limits:**
- Key: 512 bytes max
- Value: 25 MiB max
- Metadata: 1024 bytes max
- Write rate: 1/second per key (429 error if exceeded)
- Expiration: 60 seconds minimum
### List Operations
```typescript
// List with pagination
const result = await env.MY_KV.list({ prefix: 'user:', limit: 1000, cursor });
// result: { keys: [], list_complete: boolean, cursor?: string }
// CRITICAL: Always check list_complete, not keys.length === 0
let cursor: string | undefined;
do {
const result = await env.MY_KV.list({ prefix: 'user:', cursor });
processKeys(result.keys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
```
### Delete Operations
```typescript
// Delete single key
await env.MY_KV.delete('key'); // Always succeeds
// Bulk delete (CLI only, up to 10,000 keys)
// npx wrangler kv bulk delete --binding=MY_KV keys.json
```
---
## Advanced Patterns
### Caching Pattern with CacheTtl
```typescript
async function getCachedData(kv: KVNamespace, key: string, fetchFn: () => Promise<any>, ttl = 300) {
const cached = await kv.get(key, { type: 'json', cacheTtl: ttl });
if (cached) return cached;
const data = await fetchFn();
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl * 2 });
return data;
}
```
**Guidelines**: Minimum 60s, use for read-heavy workloads (100:1 read/write ratio)
### Metadata Optimization
```typescript
// Store small values (<1024 bytes) in metadata to avoid separate get() calls
await env.MY_KV.put('user:123', '', {
metadata: { status: 'active', plan: 'pro', lastSeen: Date.now() }
});
// list() returns metadata automatically (no additional get() calls)
const users = await env.MY_KV.list({ prefix: 'user:' });
users.keys.forEach(({ name, metadata }) => console.log(name, metadata.status));
```
### Key Coalescing
```typescript
// ❌ Bad: Many cold keys
await kv.put('user:123:name', 'John');
await kv.put('user:123:email', 'john@example.com');
// ✅ Good: Single hot key
await kv.put('user:123', JSON.stringify({ name: 'John', email: 'john@example.com' }));
```
**Benefit**: Cold keys benefit from hot key caching, fewer operations
**Trade-off**: Requires read-modify-write for updates
### Pagination Helper
```typescript
async function* paginateKV(kv: KVNamespace, options: { prefix?: string } = {}) {
let cursor: string | undefined;
do {
const result = await kv.list({ ...options, cursor });
yield result.keys;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
}
// Usage
for await (const keys of paginateKV(env.MY_KV, { prefix: 'user:' })) {
processKeys(keys);
}
```
### Rate Limit Retry with Exponential Backoff
```typescript
async function putWithRetry(kv: KVNamespace, key: string, value: string, opts?: KVPutOptions) {
let attempts = 0, delay = 1000;
while (attempts < 5) {
try {
await kv.put(key, value, opts);
return;
} catch (error) {
if ((error as Error).message.includes('429')) {
attempts++;
if (attempts >= 5) throw new Error('Max retry attempts');
await new Promise(r => setTimeout(r, delay));
delay *= 2; // Exponential backoff
} else throw error;
}
}
}
```
---
## Understanding Eventual Consistency
KV is **eventually consistent** across Cloudflare's global network (Aug 2025 redesign: hybrid storage, <5ms p99 latency):
**How It Works:**
1. Writes immediately visible in same location
2. Other locations see update within ~60 seconds (or cacheTtl value)
3. Cached reads may return stale data during propagation
**Example:**
```typescript
// Tokyo: Write
await env.MY_KV.put('counter', '1');
const value = await env.MY_KV.get('counter'); // "1" ✅
// London (within 60s): May be stale ⚠️
const value2 = await env.MY_KV.get('counter'); // Might be old value
// After 60+ seconds: Consistent ✅
```
**Use KV for**: Read-heavy workloads (100:1 ratio), config, feature flags, caching, user preferences
**Don't use KV for**: Financial transactions, strong consistency, >1/second writes per key, critical data
**Need strong consistency?** Use [Durable Objects](https://developers.cloudflare.com/durable-objects/)
---
## Wrangler CLI Essentials
```bash
# Create namespace
npx wrangler kv namespace create MY_NAMESPACE [--preview]
# Manage keys
npx wrangler kv key put --binding=MY_KV "key" "value" [--ttl=3600] [--metadata='{}']
npx wrangler kv key get --binding=MY_KV "key"
npx wrangler kv key list --binding=MY_KV [--prefix="user:"]
npx wrangler kv key delete --binding=MY_KV "key"
# Bulk operations (up to 10,000 keys)
npx wrangler kv bulk put --binding=MY_KV data.json
npx wrangler kv bulk delete --binding=MY_KV keys.json
```
---
## Limits & Quotas
| Feature | Free Plan | Paid Plan |
|---------|-----------|-----------|
| Reads per day | 100,000 | Unlimited |
| Writes per day (different keys) | 1,000 | Unlimited |
| **Writes per key per second** | **1** | **1** |
| Operations per Worker invocation | 1,000 | 1,000 |
| **Namespaces per account** | **1,000** | **1,000** |
| Storage per account | 1 GB | Unlimited |
| Key size | 512 bytes | 512 bytes |
| Metadata size | 1024 bytes | 1024 bytes |
| Value size | 25 MiB | 25 MiB |
| Minimum cacheTtl | 60 seconds | 60 seconds |
**Critical**: 1 write/second per key (429 if exceeded), bulk operations count as 1 operation, namespace limit increased from 200 → 1,000 (Jan 2025)
---
## Error Handling
### 1. Rate Limit (429 Too Many Requests)
**Cause**: Writing to same key >1/second
**Solution**: Use retry with exponential backoff (see Advanced Patterns)
```typescript
// ❌ Bad
await env.MY_KV.put('counter', '1');
await env.MY_KV.put('counter', '2'); // 429 error!
// ✅ Good
await putWithRetry(env.MY_KV, 'counter', '2');
```
### 2. Value Too Large
**Cause**: Value exceeds 25 MiB
**Solution**: Validate size before writing
```typescript
if (value.length > 25 * 1024 * 1024) throw new Error('Value exceeds 25 MiB');
```
### 3. Metadata Too Large
**Cause**: Metadata exceeds 1024 bytes when serialized
**Solution**: Validate serialized size
```typescript
const serialized = JSON.stringify(metadata);
if (serialized.length > 1024) throw new Error('Metadata exceeds 1024 bytes');
```
### 4. Invalid CacheTtl
**Cause**: cacheTtl <60 seconds
**Solution**: Use minimum 60
```typescript
// ❌ Error
await env.MY_KV.get('key', { cacheTtl: 30 });
// ✅ Correct
await env.MY_KV.get('key', { cacheTtl: 60 });
```
---
## Critical Rules
### Always Do ✅
1. Use bulk operations when reading multiple keys (counts as 1 operation)
2. Set cacheTtl for frequently-read, infrequently-updated data (min 60s)
3. Store small values (<1024 bytes) in metadata when using `list()` frequently
4. Check `list_complete` when paginating, not `keys.length === 0`
5. Use retry logic with exponential backoff for write operations
6. Validate sizes before writing (key 512B, value 25MiB, metadata 1KB)
7. Coalesce related keys for better caching performance
8. Use KV for read-heavy workloads (100:1 read/write ratio ideal)
### Never Do ❌
1. Never write to same key >1/second (causes 429 rate limit errors)
2. Never assume immediate global consistency (takes ~60 seconds to propagate)
3. Never use KV for atomic operations (use Durable Objects instead)
4. Never set cacheTtl <60 seconds (will fail)
5. Never commit namespace IDs to public repos (use environment variables)
6. Never exceed 1000 operations per invocation (use bulk operations)
7. Never rely on write order (eventual consistency = no guarantees)
8. Never forget to handle null values (`get()` returns `null` if key doesn't exist)
---
## Troubleshooting
### Issue 1: "429 Too Many Requests" on writes
**Cause**: Writing to same key >1/second
**Solution**: Consolidate writes or use retry with exponential backoff
```typescript
// ❌ Bad: Rate limit
for (let i = 0; i < 10; i++) await kv.put('counter', String(i));
// ✅ Good: Single write
await kv.put('counter', '9');
// ✅ Good: Retry with backoff
await putWithRetry(kv, 'counter', String(i));
```
### Issue 2: Stale reads after write
**Cause**: Eventual consistency (~60 seconds propagation)
**Solution**: Accept stale reads, use Durable Objects for strong consistency, or implement app-level cache invalidation
### Issue 3: "Operations limit exceeded"
**Cause**: >1000 KV operations in single Worker invocation
**Solution**: Use bulk operations
```typescript
// ❌ Bad: 5000 operations
for (const key of 5000keys) await kv.get(key);
// ✅ Good: 1 operation
const values = await kv.get(keys); // Bulk read
```
### Issue 4: List returns empty but cursor exists
**Cause**: Deleted/expired keys create "tombstones"
**Solution**: Always check `list_complete`, not `keys.length`
```typescript
// ✅ Correct pagination
let cursor: string | undefined;
do {
const result = await kv.list({ cursor });
processKeys(result.keys); // Even if empty
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
```
---
## Production Checklist
- [ ] Environment-specific namespaces configured (`id` vs `preview_id`)
- [ ] Namespace IDs stored in environment variables (not hardcoded)
- [ ] Rate limit retry logic implemented for writes
- [ ] Appropriate `cacheTtl` values set for reads (min 60s)
- [ ] Sizes validated (key 512B, value 25MiB, metadata 1KB)
- [ ] Bulk operations used where possible
- [ ] Pagination with `list_complete` check (not `keys.length`)
- [ ] Error handling for null values
- [ ] Monitoring/alerting for rate limits
---
## Related Documentation
- [Cloudflare KV Docs](https://developers.cloudflare.com/kv/)
- [KV API Reference](https://developers.cloudflare.com/kv/api/)
- [KV Limits](https://developers.cloudflare.com/kv/platform/limits/)
- [How KV Works](https://developers.cloudflare.com/kv/concepts/how-kv-works/)
- [Wrangler KV Commands](https://developers.cloudflare.com/workers/wrangler/commands/#kv)
---
**Last Updated**: 2025-11-24
**Package Versions**: wrangler@4.50.0, @cloudflare/workers-types@4.20251121.0

73
plugin.lock.json Normal file
View File

@@ -0,0 +1,73 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:jezweb/claude-skills:skills/cloudflare-kv",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "ce8453586663e42be92dc68ac0d9688e1b5f3bca",
"treeHash": "91ed0423c52d84b688f4dbc23f5e35d4906098a8c19ee220485da4e066ebf4aa",
"generatedAt": "2025-11-28T10:18:56.373969Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "cloudflare-kv",
"description": "Store key-value data globally with Cloudflare KVs edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "d2d5798f1e8aecce1df20b5de0992ccb2c942ff8740492203842fa5296d8c214"
},
{
"path": "SKILL.md",
"sha256": "0bbb7062c7baae72de902f85520d307f996d65c6a11be9a4b45014b0f3534e17"
},
{
"path": "references/best-practices.md",
"sha256": "0adc9505c4d050e21db41917daaa7e9bfa37b6c558c0a910056045938aa45f58"
},
{
"path": "references/workers-api.md",
"sha256": "d3283b7c8395c3eb22936522185aa08ef1948802c3501a1532c68475aa819974"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "858fd909ff72e8b3013fa0de105b29ebee5ae2526c336d950d6880060653f62d"
},
{
"path": "templates/wrangler-kv-config.jsonc",
"sha256": "52694be4cfd90f6e2d5501d0da0e2910e2b041b5c8b86140e08020e9a6fc5131"
},
{
"path": "templates/kv-metadata-pattern.ts",
"sha256": "fbb3377064dc79b52a93ebd8ea6fdced88bc179981d62c985c3eb13c1e89c480"
},
{
"path": "templates/kv-list-pagination.ts",
"sha256": "9efca329c0a35a7b47acd4671245b46191c9413d2d80286408404acdfa226e0a"
},
{
"path": "templates/kv-basic-operations.ts",
"sha256": "850dd2be4f78ae2fac9057bf44424a7b94202c91ba2b789b73e65c8a3f67f798"
},
{
"path": "templates/kv-caching-pattern.ts",
"sha256": "1e16eb83f3e355b2e5c5f90ef470d9c7433fde3d1cc1d8d015073491d9fc459f"
}
],
"dirSha256": "91ed0423c52d84b688f4dbc23f5e35d4906098a8c19ee220485da4e066ebf4aa"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,720 @@
# Cloudflare Workers KV - Best Practices
This document contains production-tested best practices for Cloudflare Workers KV.
---
## Table of Contents
1. [Performance Optimization](#performance-optimization)
2. [Caching Strategies](#caching-strategies)
3. [Key Design](#key-design)
4. [Metadata Usage](#metadata-usage)
5. [Error Handling](#error-handling)
6. [Security](#security)
7. [Cost Optimization](#cost-optimization)
8. [Monitoring & Debugging](#monitoring--debugging)
---
## Performance Optimization
### 1. Use Bulk Operations
**❌ Bad: Individual reads**
```typescript
const value1 = await kv.get('key1'); // 1 operation
const value2 = await kv.get('key2'); // 1 operation
const value3 = await kv.get('key3'); // 1 operation
// Total: 3 operations
```
**✅ Good: Bulk read**
```typescript
const values = await kv.get(['key1', 'key2', 'key3']); // 1 operation
// Total: 1 operation
```
**Benefits:**
- Counts as 1 operation against the 1000/invocation limit
- Faster execution
- Lower latency
---
### 2. Use CacheTtl for Frequently-Read Data
**❌ Bad: No edge caching**
```typescript
const value = await kv.get('config'); // Fetches from KV every time
```
**✅ Good: Edge caching**
```typescript
const value = await kv.get('config', {
cacheTtl: 300, // Cache at edge for 5 minutes
});
```
**Guidelines:**
- Use `cacheTtl` for data that changes infrequently
- Minimum: 60 seconds
- Typical values:
- Configuration: 300-600 seconds (5-10 minutes)
- Static content: 3600+ seconds (1+ hour)
- Frequently changing: 60-120 seconds
**Trade-off:** Higher `cacheTtl` = faster reads but slower updates propagate
---
### 3. Coalesce Related Keys
**❌ Bad: Many small keys**
```typescript
await kv.put('user:123:name', 'John');
await kv.put('user:123:email', 'john@example.com');
await kv.put('user:123:age', '30');
// Reading requires 3 operations
const name = await kv.get('user:123:name');
const email = await kv.get('user:123:email');
const age = await kv.get('user:123:age');
```
**✅ Good: Coalesced key**
```typescript
await kv.put('user:123', JSON.stringify({
name: 'John',
email: 'john@example.com',
age: 30,
}));
// Reading requires 1 operation
const user = await kv.get<User>('user:123', { type: 'json' });
```
**Benefits:**
- Fewer operations
- Single cache entry
- Faster reads
**When to use:**
- Related data that's always accessed together
- Data that doesn't update frequently
- Values stay under 25 MiB total
---
### 4. Store Small Values in Metadata
**❌ Bad: Separate keys for metadata**
```typescript
await kv.put('user:123', 'data');
await kv.put('user:123:status', 'active');
// List requires additional get() calls
const users = await kv.list({ prefix: 'user:' });
for (const key of users.keys) {
const status = await kv.get(`${key.name}:status`); // Extra operation!
}
```
**✅ Good: Metadata pattern**
```typescript
await kv.put('user:123', 'data', {
metadata: { status: 'active', plan: 'pro' },
});
// List includes metadata, no extra get() calls!
const users = await kv.list({ prefix: 'user:' });
for (const key of users.keys) {
console.log(key.name, key.metadata.status); // No extra operations
}
```
**When to use:**
- Values fit in 1024 bytes
- Frequently use `list()` operations
- Need to filter/process many keys
---
## Caching Strategies
### 1. Cache-Aside Pattern (Read-Through)
```typescript
async function getCached<T>(
kv: KVNamespace,
key: string,
fetchFn: () => Promise<T>,
ttl = 3600
): Promise<T> {
// Try cache
const cached = await kv.get<T>(key, {
type: 'json',
cacheTtl: 300,
});
if (cached !== null) return cached;
// Cache miss - fetch and store
const data = await fetchFn();
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl });
return data;
}
```
**Use when:**
- Data is expensive to compute/fetch
- Read >> Write ratio
- Acceptable to serve slightly stale data
---
### 2. Write-Through Cache
```typescript
async function updateCached<T>(
kv: KVNamespace,
key: string,
data: T,
ttl = 3600
): Promise<void> {
// Update database
await database.update(data);
// Update cache immediately
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl });
}
```
**Use when:**
- Need cache consistency
- Write operations are infrequent
- Cache must always reflect latest data
---
### 3. Stale-While-Revalidate
```typescript
async function staleWhileRevalidate<T>(
kv: KVNamespace,
key: string,
fetchFn: () => Promise<T>,
ctx: ExecutionContext,
staleThreshold = 300
): Promise<T> {
const { value, metadata } = await kv.getWithMetadata<T, { timestamp: number }>(
key,
{ type: 'json' }
);
if (value !== null && metadata) {
const age = Date.now() - metadata.timestamp;
// Refresh in background if stale
if (age > staleThreshold * 1000) {
ctx.waitUntil(
(async () => {
const fresh = await fetchFn();
await kv.put(key, JSON.stringify(fresh), {
metadata: { timestamp: Date.now() },
});
})()
);
}
return value;
}
// Cache miss
const data = await fetchFn();
await kv.put(key, JSON.stringify(data), {
metadata: { timestamp: Date.now() },
});
return data;
}
```
**Use when:**
- Fast response time is critical
- Acceptable to serve slightly stale data
- Background refresh is acceptable
---
## Key Design
### 1. Use Hierarchical Namespaces
**✅ Good key patterns:**
```
user:123:profile
user:123:settings
user:123:sessions
session:abc123:data
session:abc123:metadata
cache:api:users:list
cache:api:posts:123
cache:db:query:hash123
```
**Benefits:**
- Easy to filter with `list({ prefix: 'user:123:' })`
- Easy to invalidate groups
- Clear organization
---
### 2. Use Lexicographic Ordering
Keys are always sorted lexicographically, so design keys to take advantage:
```typescript
// Date-based keys (ISO format sorts correctly)
'log:2025-10-21:entry1'
'log:2025-10-22:entry1'
// Numeric IDs (zero-padded)
'user:00000001'
'user:00000123'
'user:00001000'
// Priority-based (prefix with number)
'task:1:high-priority'
'task:2:medium-priority'
'task:3:low-priority'
```
---
### 3. Avoid Key Collisions
**❌ Bad:**
```
user:123 // User data
user:123:count // Some counter
user:123 // Different data? Collision!
```
**✅ Good:**
```
user:data:123
user:counter:123
user:session:123
```
---
## Metadata Usage
### 1. Track Versions
```typescript
await kv.put('config', JSON.stringify(data), {
metadata: {
version: 2,
updatedAt: Date.now(),
updatedBy: 'admin',
},
});
```
---
### 2. Audit Trails
```typescript
await kv.put(key, value, {
metadata: {
createdAt: Date.now(),
createdBy: userId,
accessCount: 0,
},
});
```
---
### 3. Feature Flags
```typescript
// Store flags in metadata for fast list() access
await kv.put(`flag:${name}`, JSON.stringify(config), {
metadata: {
enabled: true,
rolloutPercentage: 50,
},
});
// List all flags without additional get() calls
const flags = await kv.list({ prefix: 'flag:' });
```
---
## Error Handling
### 1. Handle Rate Limits (429)
```typescript
async function putWithRetry(
kv: KVNamespace,
key: string,
value: string,
maxAttempts = 5
): Promise<void> {
let attempts = 0;
let delay = 1000;
while (attempts < maxAttempts) {
try {
await kv.put(key, value);
return;
} catch (error) {
const message = (error as Error).message;
if (message.includes('429') || message.includes('Too Many Requests')) {
attempts++;
if (attempts >= maxAttempts) {
throw new Error('Max retry attempts reached');
}
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
} else {
throw error;
}
}
}
}
```
---
### 2. Handle Null Values
```typescript
// ❌ Bad
const value = await kv.get('key');
console.log(value.toUpperCase()); // Error if key doesn't exist
// ✅ Good
const value = await kv.get('key');
if (value !== null) {
console.log(value.toUpperCase());
}
// ✅ Good: Default value
const value = await kv.get('key') ?? 'default';
```
---
### 3. Validate Input Sizes
```typescript
function validateKVInput(key: string, value: string, metadata?: any): void {
// Key size
if (new TextEncoder().encode(key).length > 512) {
throw new Error('Key exceeds 512 bytes');
}
// Value size
if (new TextEncoder().encode(value).length > 25 * 1024 * 1024) {
throw new Error('Value exceeds 25 MiB');
}
// Metadata size
if (metadata) {
const serialized = JSON.stringify(metadata);
if (new TextEncoder().encode(serialized).length > 1024) {
throw new Error('Metadata exceeds 1024 bytes');
}
}
}
```
---
## Security
### 1. Never Commit Namespace IDs
**❌ Bad:**
```jsonc
{
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "abc123def456..." // Hardcoded!
}
]
}
```
**✅ Good:**
```jsonc
{
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "${KV_NAMESPACE_ID}" // Environment variable
}
]
}
```
---
### 2. Encrypt Sensitive Data
```typescript
// Encrypt before storing
const encrypted = await encrypt(sensitiveData, encryptionKey);
await kv.put('sensitive:123', encrypted);
// Decrypt after reading
const encrypted = await kv.get('sensitive:123');
const decrypted = await decrypt(encrypted, encryptionKey);
```
---
### 3. Use Separate Namespaces for Environments
```jsonc
{
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "production-namespace-id",
"preview_id": "development-namespace-id"
}
]
}
```
---
## Cost Optimization
### 1. Minimize Write Operations (Free Tier)
**Free tier limits:**
- 1,000 writes per day
- 100,000 reads per day
**Strategies:**
- Batch writes when possible
- Use longer TTLs to reduce rewrites
- Cache data in memory if accessed frequently within same invocation
---
### 2. Use Metadata Instead of Separate Keys
**❌ Expensive: 3 writes, 3 reads**
```typescript
await kv.put('user:123:status', 'active');
await kv.put('user:123:plan', 'pro');
await kv.put('user:123:updated', Date.now().toString());
```
**✅ Cheaper: 1 write, 1 read**
```typescript
await kv.put('user:123', '', {
metadata: { status: 'active', plan: 'pro', updated: Date.now() },
});
```
---
### 3. Set Appropriate TTLs
Longer TTLs = fewer rewrites = lower costs
```typescript
// ❌ Expensive: Rewrites every minute
await kv.put('cache:data', data, { expirationTtl: 60 });
// ✅ Better: Rewrites every hour
await kv.put('cache:data', data, { expirationTtl: 3600 });
```
---
## Monitoring & Debugging
### 1. Track Cache Hit Rates
```typescript
let stats = { hits: 0, misses: 0 };
async function getCached<T>(kv: KVNamespace, key: string): Promise<T | null> {
const value = await kv.get<T>(key, { type: 'json' });
if (value !== null) {
stats.hits++;
} else {
stats.misses++;
}
return value;
}
// View stats
app.get('/stats', (c) => {
const total = stats.hits + stats.misses;
const hitRate = total > 0 ? (stats.hits / total) * 100 : 0;
return c.json({
hits: stats.hits,
misses: stats.misses,
hitRate: `${hitRate.toFixed(2)}%`,
});
});
```
---
### 2. Log KV Operations
```typescript
async function loggedGet<T>(
kv: KVNamespace,
key: string
): Promise<T | null> {
const start = Date.now();
const value = await kv.get<T>(key, { type: 'json' });
const duration = Date.now() - start;
console.log({
operation: 'get',
key,
found: value !== null,
duration,
});
return value;
}
```
---
### 3. Use Namespace Prefixes for Testing
```typescript
const namespace = env.ENVIRONMENT === 'production' ? 'prod' : 'test';
await kv.put(`${namespace}:user:123`, data);
// Cleanup test data
if (env.ENVIRONMENT === 'test') {
// Delete all test: keys
let cursor: string | undefined;
do {
const result = await kv.list({ prefix: 'test:', cursor });
await Promise.all(result.keys.map(k => kv.delete(k.name)));
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
}
```
---
## Production Checklist
Before deploying to production:
- [ ] Environment-specific namespaces configured
- [ ] Namespace IDs stored in environment variables
- [ ] Rate limit retry logic implemented
- [ ] Appropriate `cacheTtl` values set
- [ ] Input validation for key/value/metadata sizes
- [ ] Bulk operations used where possible
- [ ] Pagination implemented correctly for `list()`
- [ ] Error handling for null values
- [ ] Monitoring/alerting for rate limits
- [ ] Documentation for eventual consistency behavior
- [ ] Security review for sensitive data
- [ ] Cost analysis for expected usage
---
## Common Patterns
### 1. Session Management
```typescript
// Store session
await kv.put(`session:${sessionId}`, JSON.stringify(sessionData), {
expirationTtl: 3600, // 1 hour
metadata: { userId, createdAt: Date.now() },
});
// Read session
const session = await kv.get<SessionData>(`session:${sessionId}`, {
type: 'json',
cacheTtl: 60, // Cache for 1 minute
});
```
---
### 2. API Response Caching
```typescript
const cacheKey = `api:${endpoint}:${JSON.stringify(params)}`;
let response = await kv.get<ApiResponse>(cacheKey, {
type: 'json',
cacheTtl: 300,
});
if (!response) {
response = await fetchFromAPI(endpoint, params);
await kv.put(cacheKey, JSON.stringify(response), {
expirationTtl: 600,
});
}
return response;
```
---
### 3. Configuration Management
```typescript
// Update config
await kv.put('config:app', JSON.stringify(config), {
metadata: {
version: 2,
updatedAt: Date.now(),
updatedBy: adminId,
},
});
// Read config (with long cache)
const config = await kv.get<AppConfig>('config:app', {
type: 'json',
cacheTtl: 3600, // Cache for 1 hour
});
```
---
## References
- [Cloudflare KV Documentation](https://developers.cloudflare.com/kv/)
- [How KV Works](https://developers.cloudflare.com/kv/concepts/how-kv-works/)
- [KV Limits](https://developers.cloudflare.com/kv/platform/limits/)
- [KV Pricing](https://developers.cloudflare.com/kv/platform/pricing/)

483
references/workers-api.md Normal file
View File

@@ -0,0 +1,483 @@
# Cloudflare Workers KV - Complete API Reference
This document provides the complete Workers KV API reference based on official Cloudflare documentation.
---
## KVNamespace Interface
```typescript
interface KVNamespace {
get(key: string, options?: Partial<KVGetOptions<undefined>>): Promise<string | null>;
get(key: string, type: "text"): Promise<string | null>;
get<ExpectedValue = unknown>(key: string, type: "json"): Promise<ExpectedValue | null>;
get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
get(key: string, type: "stream"): Promise<ReadableStream | null>;
get(keys: string[]): Promise<Map<string, string | null>>;
get<ExpectedValue = unknown>(keys: string[], type: "json"): Promise<Map<string, ExpectedValue | null>>;
getWithMetadata<Metadata = unknown>(key: string, options?: Partial<KVGetOptions<undefined>>): Promise<KVGetWithMetadataResult<string, Metadata>>;
getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: string, type: "json"): Promise<KVGetWithMetadataResult<ExpectedValue, Metadata>>;
getWithMetadata<Metadata = unknown>(keys: string[]): Promise<Map<string, KVGetWithMetadataResult<string, Metadata>>>;
put(key: string, value: string | ArrayBuffer | ArrayBufferView | ReadableStream, options?: KVPutOptions): Promise<void>;
delete(key: string): Promise<void>;
list<Metadata = unknown>(options?: KVListOptions): Promise<KVListResult<Metadata>>;
}
```
---
## Read Operations
### `get()` - Single Key
Read a single key-value pair.
**Signature:**
```typescript
get(key: string, options?: KVGetOptions): Promise<T | null>
```
**Parameters:**
- `key` (string, required) - The key to read
- `options` (object, optional):
- `type` - Return type: `"text"` (default), `"json"`, `"arrayBuffer"`, `"stream"`
- `cacheTtl` (number) - Edge cache duration in seconds (minimum: 60)
**Returns:**
- `Promise<T | null>` - Value or `null` if key doesn't exist
**Examples:**
```typescript
// Text (default)
const value = await env.MY_KV.get('my-key');
// JSON
const data = await env.MY_KV.get<MyType>('my-key', { type: 'json' });
// With cache optimization
const value = await env.MY_KV.get('my-key', {
type: 'text',
cacheTtl: 300, // Cache for 5 minutes
});
// ArrayBuffer
const buffer = await env.MY_KV.get('binary-key', { type: 'arrayBuffer' });
// Stream (for large values)
const stream = await env.MY_KV.get('large-file', { type: 'stream' });
```
---
### `get()` - Multiple Keys (Bulk)
Read multiple keys in a single operation.
**Signature:**
```typescript
get(keys: string[], type?: 'text' | 'json'): Promise<Map<string, T | null>>
```
**Parameters:**
- `keys` (string[], required) - Array of keys to read
- `type` (optional) - Return type: `"text"` (default) or `"json"`
**Returns:**
- `Promise<Map<string, T | null>>` - Map of key-value pairs
**Important:**
- Counts as **1 operation** regardless of number of keys
- Only supports `text` and `json` types (not `arrayBuffer` or `stream`)
- For binary/stream types, use individual `get()` calls with `Promise.all()`
**Examples:**
```typescript
// Read multiple keys
const keys = ['key1', 'key2', 'key3'];
const values = await env.MY_KV.get(keys);
// Access values
const value1 = values.get('key1');
const value2 = values.get('key2');
// Convert to object
const obj = Object.fromEntries(values);
// Read as JSON
const values = await env.MY_KV.get<MyType>(keys, 'json');
```
---
### `getWithMetadata()` - Single Key
Read key-value pair with metadata.
**Signature:**
```typescript
getWithMetadata<Value, Metadata>(
key: string,
options?: KVGetOptions
): Promise<KVGetWithMetadataResult<Value, Metadata>>
```
**Parameters:**
- Same as `get()`
**Returns:**
```typescript
{
value: Value | null,
metadata: Metadata | null
}
```
**Examples:**
```typescript
// Get with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata('my-key');
// Get as JSON with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata<MyType>('my-key', {
type: 'json',
cacheTtl: 300,
});
if (value !== null) {
console.log('Value:', value);
console.log('Metadata:', metadata);
}
```
---
### `getWithMetadata()` - Multiple Keys (Bulk)
Read multiple keys with metadata.
**Signature:**
```typescript
getWithMetadata<Metadata>(
keys: string[],
type?: 'text' | 'json'
): Promise<Map<string, KVGetWithMetadataResult<T, Metadata>>>
```
**Examples:**
```typescript
const keys = ['key1', 'key2'];
const results = await env.MY_KV.getWithMetadata(keys);
for (const [key, data] of results) {
console.log(key, data.value, data.metadata);
}
```
---
## Write Operations
### `put()` - Write Key-Value Pair
Write or update a key-value pair.
**Signature:**
```typescript
put(
key: string,
value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
options?: KVPutOptions
): Promise<void>
```
**Parameters:**
- `key` (string, required) - Maximum 512 bytes
- `value` (required) - Maximum 25 MiB
- `options` (object, optional):
- `expiration` (number) - Absolute expiration time (seconds since epoch)
- `expirationTtl` (number) - TTL in seconds from now (minimum: 60)
- `metadata` (any) - JSON-serializable metadata (maximum: 1024 bytes)
**Returns:**
- `Promise<void>`
**Examples:**
```typescript
// Simple write
await env.MY_KV.put('key', 'value');
// Write JSON
await env.MY_KV.put('user:123', JSON.stringify({ name: 'John' }));
// Write with TTL
await env.MY_KV.put('session', sessionData, {
expirationTtl: 3600, // Expire in 1 hour
});
// Write with absolute expiration
const expirationTime = Math.floor(Date.now() / 1000) + 86400; // 24 hours
await env.MY_KV.put('token', tokenValue, {
expiration: expirationTime,
});
// Write with metadata
await env.MY_KV.put('config', configData, {
metadata: {
updatedAt: Date.now(),
updatedBy: 'admin',
version: 2,
},
});
// Write with everything
await env.MY_KV.put('key', 'value', {
expirationTtl: 600,
metadata: { source: 'api' },
});
```
**Limits:**
- **Key size**: Maximum 512 bytes
- **Value size**: Maximum 25 MiB
- **Metadata size**: Maximum 1024 bytes (JSON serialized)
- **Write rate**: Maximum **1 write per second per key**
- **Expiration minimum**: 60 seconds
---
## Delete Operations
### `delete()` - Delete Key
Delete a key-value pair.
**Signature:**
```typescript
delete(key: string): Promise<void>
```
**Parameters:**
- `key` (string, required) - Key to delete
**Returns:**
- `Promise<void>` - Always succeeds, even if key doesn't exist
**Examples:**
```typescript
// Delete single key
await env.MY_KV.delete('my-key');
// Delete multiple keys
const keys = ['key1', 'key2', 'key3'];
await Promise.all(keys.map(key => env.MY_KV.delete(key)));
```
**Note:** For bulk delete of >10,000 keys, use the REST API.
---
## List Operations
### `list()` - List Keys
List keys in the namespace.
**Signature:**
```typescript
list<Metadata>(options?: KVListOptions): Promise<KVListResult<Metadata>>
```
**Parameters:**
```typescript
interface KVListOptions {
prefix?: string; // Filter keys by prefix
limit?: number; // Max keys to return (default: 1000, max: 1000)
cursor?: string; // Pagination cursor
}
```
**Returns:**
```typescript
interface KVListResult<Metadata> {
keys: {
name: string;
expiration?: number; // Seconds since epoch
metadata?: Metadata;
}[];
list_complete: boolean; // true if no more keys
cursor?: string; // Use for next page
}
```
**Examples:**
```typescript
// List all keys (up to 1000)
const result = await env.MY_KV.list();
// List with prefix
const users = await env.MY_KV.list({ prefix: 'user:' });
// List with limit
const recent = await env.MY_KV.list({ limit: 100 });
// Pagination
let cursor: string | undefined;
do {
const result = await env.MY_KV.list({ cursor });
// Process keys
console.log(result.keys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
```
**Important:**
- Keys are **always** sorted lexicographically (UTF-8)
- **Always check `list_complete`**, not `keys.length === 0`
- Empty `keys` array doesn't mean no more data (tombstones exist)
- When paginating with `prefix`, pass `prefix` with each cursor request
---
## Type Definitions
### KVGetOptions
```typescript
interface KVGetOptions<Type> {
type: Type; // "text" | "json" | "arrayBuffer" | "stream"
cacheTtl?: number; // Edge cache duration (minimum: 60 seconds)
}
```
### KVPutOptions
```typescript
interface KVPutOptions {
expiration?: number; // Seconds since epoch
expirationTtl?: number; // Seconds from now (minimum: 60)
metadata?: any; // Max 1024 bytes serialized
}
```
### KVGetWithMetadataResult
```typescript
interface KVGetWithMetadataResult<Value, Metadata> {
value: Value | null;
metadata: Metadata | null;
}
```
### KVListOptions
```typescript
interface KVListOptions {
prefix?: string;
limit?: number; // Default: 1000, max: 1000
cursor?: string;
}
```
### KVListResult
```typescript
interface KVListResult<Metadata = unknown> {
keys: {
name: string;
expiration?: number;
metadata?: Metadata;
}[];
list_complete: boolean;
cursor?: string;
}
```
---
## Limits
| Feature | Limit |
|---------|-------|
| Key size | 512 bytes |
| Value size | 25 MiB |
| Metadata size | 1024 bytes (JSON) |
| Writes per key per second | 1 |
| Operations per Worker invocation | 1,000 |
| List limit | 1,000 keys |
| Minimum cacheTtl | 60 seconds |
| Minimum expiration | 60 seconds |
| Namespaces per account (Free) | 1,000 |
| Namespaces per account (Paid) | 1,000 |
| Storage per account (Free) | 1 GB |
| Storage per account (Paid) | Unlimited |
| Read operations per day (Free) | 100,000 |
| Read operations per day (Paid) | Unlimited |
| Write operations per day (Free) | 1,000 |
| Write operations per day (Paid) | Unlimited |
---
## Consistency Model
### Eventually Consistent
- Writes are **immediately visible** in the same location
- Writes take **up to 60 seconds** to propagate globally
- Cached reads may return stale data during propagation
### Implications
```typescript
// Tokyo datacenter
await env.KV.put('counter', '1');
const value1 = await env.KV.get('counter'); // "1" ✅
// London datacenter (within 60 seconds)
const value2 = await env.KV.get('counter'); // Might be old value ⚠️
// After 60+ seconds globally
const value3 = await env.KV.get('counter'); // "1" ✅
```
**For strong consistency, use [Durable Objects](https://developers.cloudflare.com/durable-objects/).**
---
## Error Handling
### Common Errors
1. **429 Too Many Requests**
- Cause: >1 write/second to same key
- Solution: Implement retry with exponential backoff
2. **Value too large**
- Cause: Value >25 MiB
- Solution: Validate size before writing
3. **Metadata too large**
- Cause: Metadata >1024 bytes serialized
- Solution: Validate JSON size before writing
4. **Invalid cacheTtl**
- Cause: cacheTtl <60 seconds
- Solution: Use minimum 60 seconds
5. **Operations limit exceeded**
- Cause: >1000 KV operations in Worker invocation
- Solution: Use bulk operations
---
## References
- [Official KV Documentation](https://developers.cloudflare.com/kv/)
- [KV API Reference](https://developers.cloudflare.com/kv/api/)
- [KV Limits](https://developers.cloudflare.com/kv/platform/limits/)
- [How KV Works](https://developers.cloudflare.com/kv/concepts/how-kv-works/)

View File

@@ -0,0 +1,538 @@
/**
* Cloudflare Workers KV - Basic CRUD Operations
*
* This template demonstrates all basic KV operations:
* - Create (PUT)
* - Read (GET)
* - Update (PUT)
* - Delete (DELETE)
* - List keys
* - Metadata handling
* - TTL/Expiration
* - Error handling
*/
import { Hono } from 'hono';
type Bindings = {
MY_KV: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// CREATE / UPDATE - Write key-value pairs
// ============================================================================
// Simple write
app.put('/kv/:key', async (c) => {
const key = c.req.param('key');
const value = await c.req.text();
try {
await c.env.MY_KV.put(key, value);
return c.json({
success: true,
message: `Key "${key}" created/updated`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Write with TTL expiration
app.put('/kv/:key/ttl/:seconds', async (c) => {
const key = c.req.param('key');
const ttl = parseInt(c.req.param('seconds'), 10);
const value = await c.req.text();
// Validate TTL (minimum 60 seconds)
if (ttl < 60) {
return c.json(
{
success: false,
error: 'TTL must be at least 60 seconds',
},
400
);
}
try {
await c.env.MY_KV.put(key, value, {
expirationTtl: ttl,
});
return c.json({
success: true,
message: `Key "${key}" will expire in ${ttl} seconds`,
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Write with metadata
app.put('/kv/:key/metadata', async (c) => {
const key = c.req.param('key');
const body = await c.req.json<{ value: string; metadata: any }>();
// Validate metadata size (max 1024 bytes serialized)
const metadataJson = JSON.stringify(body.metadata);
if (metadataJson.length > 1024) {
return c.json(
{
success: false,
error: `Metadata too large: ${metadataJson.length} bytes (max 1024)`,
},
400
);
}
try {
await c.env.MY_KV.put(key, body.value, {
metadata: body.metadata,
});
return c.json({
success: true,
message: `Key "${key}" created with metadata`,
metadata: body.metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Write JSON data
app.post('/kv/json/:key', async (c) => {
const key = c.req.param('key');
const data = await c.req.json();
try {
await c.env.MY_KV.put(key, JSON.stringify(data));
return c.json({
success: true,
message: `JSON data stored at key "${key}"`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// READ - Get key-value pairs
// ============================================================================
// Simple read (text)
app.get('/kv/:key', async (c) => {
const key = c.req.param('key');
try {
const value = await c.env.MY_KV.get(key);
if (value === null) {
return c.json(
{
success: false,
error: `Key "${key}" not found`,
},
404
);
}
return c.json({
success: true,
key,
value,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read JSON data
app.get('/kv/json/:key', async (c) => {
const key = c.req.param('key');
try {
const value = await c.env.MY_KV.get(key, { type: 'json' });
if (value === null) {
return c.json(
{
success: false,
error: `Key "${key}" not found`,
},
404
);
}
return c.json({
success: true,
key,
value,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read with metadata
app.get('/kv/:key/metadata', async (c) => {
const key = c.req.param('key');
try {
const { value, metadata } = await c.env.MY_KV.getWithMetadata(key);
if (value === null) {
return c.json(
{
success: false,
error: `Key "${key}" not found`,
},
404
);
}
return c.json({
success: true,
key,
value,
metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read with cache optimization
app.get('/kv/:key/cached', async (c) => {
const key = c.req.param('key');
const cacheTtl = parseInt(c.req.query('cacheTtl') || '300', 10);
// Validate cacheTtl (minimum 60 seconds)
if (cacheTtl < 60) {
return c.json(
{
success: false,
error: 'cacheTtl must be at least 60 seconds',
},
400
);
}
try {
const value = await c.env.MY_KV.get(key, {
type: 'text',
cacheTtl,
});
if (value === null) {
return c.json(
{
success: false,
error: `Key "${key}" not found`,
},
404
);
}
return c.json({
success: true,
key,
value,
cached: true,
cacheTtl,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Bulk read (multiple keys)
app.post('/kv/bulk/get', async (c) => {
const { keys } = await c.req.json<{ keys: string[] }>();
if (!Array.isArray(keys) || keys.length === 0) {
return c.json(
{
success: false,
error: 'keys must be a non-empty array',
},
400
);
}
try {
// Bulk read counts as 1 operation!
const values = await c.env.MY_KV.get(keys);
// Convert Map to object
const result: Record<string, string | null> = {};
for (const [key, value] of values) {
result[key] = value;
}
return c.json({
success: true,
count: keys.length,
values: result,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// LIST - List keys with pagination
// ============================================================================
// List all keys (with pagination)
app.get('/kv/list', async (c) => {
const prefix = c.req.query('prefix') || '';
const cursor = c.req.query('cursor');
const limit = parseInt(c.req.query('limit') || '1000', 10);
try {
const result = await c.env.MY_KV.list({
prefix,
limit,
cursor: cursor || undefined,
});
return c.json({
success: true,
keys: result.keys,
count: result.keys.length,
hasMore: !result.list_complete,
cursor: result.cursor,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// List all keys with prefix (fully paginated)
app.get('/kv/list/all/:prefix', async (c) => {
const prefix = c.req.param('prefix');
let cursor: string | undefined;
const allKeys: any[] = [];
try {
// Paginate through all keys
do {
const result = await c.env.MY_KV.list({
prefix,
cursor,
});
allKeys.push(...result.keys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
prefix,
keys: allKeys,
totalCount: allKeys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// DELETE - Delete key-value pairs
// ============================================================================
// Delete single key
app.delete('/kv/:key', async (c) => {
const key = c.req.param('key');
try {
// Delete always succeeds, even if key doesn't exist
await c.env.MY_KV.delete(key);
return c.json({
success: true,
message: `Key "${key}" deleted`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Delete multiple keys
app.post('/kv/bulk/delete', async (c) => {
const { keys } = await c.req.json<{ keys: string[] }>();
if (!Array.isArray(keys) || keys.length === 0) {
return c.json(
{
success: false,
error: 'keys must be a non-empty array',
},
400
);
}
try {
// Delete all keys in parallel
await Promise.all(keys.map((key) => c.env.MY_KV.delete(key)));
return c.json({
success: true,
message: `${keys.length} keys deleted`,
count: keys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// UTILITY - Helper endpoints
// ============================================================================
// Check if key exists
app.get('/kv/:key/exists', async (c) => {
const key = c.req.param('key');
try {
const value = await c.env.MY_KV.get(key);
return c.json({
exists: value !== null,
key,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Get namespace stats
app.get('/kv/stats', async (c) => {
try {
const result = await c.env.MY_KV.list();
let totalKeys = result.keys.length;
let cursor = result.cursor;
// Count all keys (with pagination)
while (!result.list_complete && cursor) {
const nextResult = await c.env.MY_KV.list({ cursor });
totalKeys += nextResult.keys.length;
cursor = nextResult.cursor;
}
return c.json({
success: true,
totalKeys,
sample: result.keys.slice(0, 10), // First 10 keys
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;

View File

@@ -0,0 +1,518 @@
/**
* Cloudflare Workers KV - Caching Pattern
*
* This template demonstrates optimal caching patterns with KV:
* - Cache-aside pattern (read-through cache)
* - Write-through cache
* - CacheTtl optimization for edge caching
* - Stale-while-revalidate pattern
* - Cache invalidation
*/
import { Hono } from 'hono';
type Bindings = {
CACHE: KVNamespace;
DB: D1Database; // Example: database for cache misses
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Cache-Aside Pattern (Read-Through Cache)
// ============================================================================
/**
* Generic cache-aside helper
*
* 1. Try to read from cache
* 2. On miss, fetch from source
* 3. Store in cache
* 4. Return data
*/
async function getCached<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
options: {
ttl?: number; // KV expiration (default: 3600)
cacheTtl?: number; // Edge cache TTL (default: 300)
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600; // 1 hour default
const cacheTtl = options.cacheTtl ?? 300; // 5 minutes default
// Try cache first (with edge caching)
const cached = await kv.get<T>(cacheKey, {
type: 'json',
cacheTtl: Math.max(cacheTtl, 60), // Minimum 60 seconds
});
if (cached !== null) {
return cached;
}
// Cache miss - fetch from source
const data = await fetchFn();
// Store in cache (fire-and-forget)
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: Math.max(ttl, 60), // Minimum 60 seconds
});
return data;
}
// Example: Cache API response
app.get('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const cacheKey = `user:${userId}`;
try {
const user = await getCached(
c.env.CACHE,
cacheKey,
async () => {
// Simulate database fetch
const result = await c.env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
)
.bind(userId)
.first();
if (!result) {
throw new Error('User not found');
}
return result;
},
{
ttl: 3600, // Cache in KV for 1 hour
cacheTtl: 300, // Cache at edge for 5 minutes
}
);
return c.json({
success: true,
user,
cached: true,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Write-Through Cache Pattern
// ============================================================================
/**
* Write-through cache: Update cache when data changes
*/
app.put('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const userData = await c.req.json();
const cacheKey = `user:${userId}`;
try {
// Update database
await c.env.DB.prepare(
'UPDATE users SET name = ?, email = ? WHERE id = ?'
)
.bind(userData.name, userData.email, userId)
.run();
// Update cache immediately
await c.env.CACHE.put(cacheKey, JSON.stringify(userData), {
expirationTtl: 3600,
});
return c.json({
success: true,
message: 'User updated and cache refreshed',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Invalidation
// ============================================================================
/**
* Invalidate cache when data changes
*/
app.delete('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const cacheKey = `user:${userId}`;
try {
// Delete from database
await c.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(userId).run();
// Invalidate cache
await c.env.CACHE.delete(cacheKey);
return c.json({
success: true,
message: 'User deleted and cache invalidated',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Invalidate multiple cache keys
app.post('/api/cache/invalidate', async (c) => {
const { keys } = await c.req.json<{ keys: string[] }>();
try {
// Delete all cache keys in parallel
await Promise.all(keys.map((key) => c.env.CACHE.delete(key)));
return c.json({
success: true,
message: `${keys.length} cache keys invalidated`,
count: keys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Invalidate by prefix (requires list + delete)
app.post('/api/cache/invalidate/prefix', async (c) => {
const { prefix } = await c.req.json<{ prefix: string }>();
try {
let cursor: string | undefined;
let deletedCount = 0;
// List all keys with prefix and delete them
do {
const result = await c.env.CACHE.list({ prefix, cursor });
// Delete batch in parallel
await Promise.all(result.keys.map((key) => c.env.CACHE.delete(key.name)));
deletedCount += result.keys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Cache invalidated for prefix "${prefix}"`,
deletedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Stale-While-Revalidate Pattern
// ============================================================================
/**
* Return cached data immediately, refresh in background
*/
async function staleWhileRevalidate<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
ctx: ExecutionContext,
options: {
ttl?: number;
staleThreshold?: number; // Refresh if older than this
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600;
const staleThreshold = options.staleThreshold ?? 300; // 5 minutes
// Get cached value with metadata
const { value, metadata } = await kv.getWithMetadata<
T,
{ timestamp: number }
>(cacheKey, { type: 'json' });
// If cached and not too stale, return immediately
if (value !== null && metadata) {
const age = Date.now() - metadata.timestamp;
// If stale, refresh in background
if (age > staleThreshold * 1000) {
ctx.waitUntil(
(async () => {
try {
const fresh = await fetchFn();
await kv.put(cacheKey, JSON.stringify(fresh), {
expirationTtl: ttl,
metadata: { timestamp: Date.now() },
});
} catch (error) {
console.error('Background refresh failed:', error);
}
})()
);
}
return value;
}
// Cache miss - fetch and store
const data = await fetchFn();
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: ttl,
metadata: { timestamp: Date.now() },
});
return data;
}
// Example usage
app.get('/api/stats', async (c) => {
try {
const stats = await staleWhileRevalidate(
c.env.CACHE,
'global:stats',
async () => {
// Expensive computation
const result = await c.env.DB.prepare(
'SELECT COUNT(*) as total FROM users'
).first();
return result;
},
c.executionCtx,
{
ttl: 3600, // Cache for 1 hour
staleThreshold: 300, // Refresh if older than 5 minutes
}
);
return c.json({
success: true,
stats,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Multi-Layer Cache (KV + Memory)
// ============================================================================
/**
* Two-tier cache: In-memory cache + KV cache
* Useful for frequently accessed data within same Worker instance
*/
const memoryCache = new Map<string, { value: any; expires: number }>();
async function getMultiLayerCache<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
options: {
ttl?: number;
memoryTtl?: number; // In-memory cache duration
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600;
const memoryTtl = (options.memoryTtl ?? 60) * 1000; // Convert to ms
// Check memory cache first (fastest)
const memoryCached = memoryCache.get(cacheKey);
if (memoryCached && memoryCached.expires > Date.now()) {
return memoryCached.value;
}
// Check KV cache (fast, global)
const kvCached = await kv.get<T>(cacheKey, {
type: 'json',
cacheTtl: 300,
});
if (kvCached !== null) {
// Store in memory cache
memoryCache.set(cacheKey, {
value: kvCached,
expires: Date.now() + memoryTtl,
});
return kvCached;
}
// Cache miss - fetch from source
const data = await fetchFn();
// Store in both caches
memoryCache.set(cacheKey, {
value: data,
expires: Date.now() + memoryTtl,
});
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: ttl,
});
return data;
}
// Example usage
app.get('/api/config', async (c) => {
try {
const config = await getMultiLayerCache(
c.env.CACHE,
'app:config',
async () => {
// Fetch from database or API
return {
theme: 'dark',
features: ['feature1', 'feature2'],
version: '1.0.0',
};
},
{
ttl: 3600, // KV cache: 1 hour
memoryTtl: 60, // Memory cache: 1 minute
}
);
return c.json({
success: true,
config,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Warming
// ============================================================================
/**
* Pre-populate cache with frequently accessed data
*/
app.post('/api/cache/warm', async (c) => {
try {
// Example: Warm cache with top 100 users
const topUsers = await c.env.DB.prepare(
'SELECT * FROM users ORDER BY activity DESC LIMIT 100'
).all();
// Store each user in cache
const promises = topUsers.results.map((user: any) =>
c.env.CACHE.put(`user:${user.id}`, JSON.stringify(user), {
expirationTtl: 3600,
})
);
await Promise.all(promises);
return c.json({
success: true,
message: `Warmed cache with ${topUsers.results.length} users`,
count: topUsers.results.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Statistics
// ============================================================================
/**
* Track cache hit/miss rates
*/
let cacheStats = {
hits: 0,
misses: 0,
errors: 0,
};
app.get('/api/cache/stats', (c) => {
const total = cacheStats.hits + cacheStats.misses;
const hitRate = total > 0 ? (cacheStats.hits / total) * 100 : 0;
return c.json({
success: true,
stats: {
hits: cacheStats.hits,
misses: cacheStats.misses,
errors: cacheStats.errors,
total,
hitRate: `${hitRate.toFixed(2)}%`,
},
});
});
// Reset stats
app.post('/api/cache/stats/reset', (c) => {
cacheStats = { hits: 0, misses: 0, errors: 0 };
return c.json({
success: true,
message: 'Cache stats reset',
});
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;

View File

@@ -0,0 +1,551 @@
/**
* Cloudflare Workers KV - List & Pagination Patterns
*
* This template demonstrates:
* - Basic listing with cursor pagination
* - Prefix filtering
* - Async iterator pattern
* - Batch processing
* - Key search and filtering
*/
import { Hono } from 'hono';
type Bindings = {
MY_KV: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Basic Pagination
// ============================================================================
// List keys with cursor pagination
app.get('/kv/list', async (c) => {
const prefix = c.req.query('prefix') || '';
const cursor = c.req.query('cursor');
const limit = parseInt(c.req.query('limit') || '100', 10);
try {
const result = await c.env.MY_KV.list({
prefix,
limit: Math.min(limit, 1000), // Max 1000
cursor: cursor || undefined,
});
return c.json({
success: true,
keys: result.keys.map((k) => ({
name: k.name,
expiration: k.expiration,
metadata: k.metadata,
})),
count: result.keys.length,
hasMore: !result.list_complete,
nextCursor: result.cursor,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Async Iterator Pattern
// ============================================================================
/**
* Async generator for paginating through all keys
*/
async function* paginateKeys(
kv: KVNamespace,
options: {
prefix?: string;
limit?: number;
} = {}
) {
let cursor: string | undefined;
do {
const result = await kv.list({
prefix: options.prefix,
limit: options.limit || 1000,
cursor,
});
yield result.keys;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
}
// Get all keys (fully paginated)
app.get('/kv/all', async (c) => {
const prefix = c.req.query('prefix') || '';
try {
const allKeys: any[] = [];
// Use async iterator to get all keys
for await (const batch of paginateKeys(c.env.MY_KV, { prefix })) {
allKeys.push(...batch);
}
return c.json({
success: true,
keys: allKeys.map((k) => k.name),
totalCount: allKeys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Prefix Filtering
// ============================================================================
// List keys by namespace prefix
app.get('/kv/namespace/:namespace', async (c) => {
const namespace = c.req.param('namespace');
const cursor = c.req.query('cursor');
try {
const result = await c.env.MY_KV.list({
prefix: `${namespace}:`, // e.g., "user:", "session:", "cache:"
cursor: cursor || undefined,
});
return c.json({
success: true,
namespace,
keys: result.keys,
count: result.keys.length,
hasMore: !result.list_complete,
nextCursor: result.cursor,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Count keys by prefix
app.get('/kv/count/:prefix', async (c) => {
const prefix = c.req.param('prefix');
try {
let count = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({
prefix,
cursor,
});
count += result.keys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
prefix,
count,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Batch Processing
// ============================================================================
/**
* Process keys in batches
*/
async function processBatches<T>(
kv: KVNamespace,
options: {
prefix?: string;
batchSize?: number;
},
processor: (keys: any[]) => Promise<T[]>
): Promise<T[]> {
const results: T[] = [];
let cursor: string | undefined;
do {
const result = await kv.list({
prefix: options.prefix,
limit: options.batchSize || 100,
cursor,
});
// Process this batch
const batchResults = await processor(result.keys);
results.push(...batchResults);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return results;
}
// Example: Export all keys with values
app.get('/kv/export', async (c) => {
const prefix = c.req.query('prefix') || '';
try {
const exported = await processBatches(
c.env.MY_KV,
{ prefix, batchSize: 100 },
async (keys) => {
// Get values for all keys in batch (bulk read)
const keyNames = keys.map((k) => k.name);
const values = await c.env.MY_KV.get(keyNames);
// Combine keys with values
return keys.map((key) => ({
key: key.name,
value: values.get(key.name),
metadata: key.metadata,
expiration: key.expiration,
}));
}
);
return c.json({
success: true,
data: exported,
count: exported.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Search & Filtering
// ============================================================================
// Search keys by pattern (client-side filtering)
app.get('/kv/search', async (c) => {
const query = c.req.query('q') || '';
const prefix = c.req.query('prefix') || '';
if (!query) {
return c.json(
{
success: false,
error: 'Query parameter "q" is required',
},
400
);
}
try {
const matches: any[] = [];
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({
prefix,
cursor,
});
// Filter keys that match the search query
const filteredKeys = result.keys.filter((key) =>
key.name.toLowerCase().includes(query.toLowerCase())
);
matches.push(...filteredKeys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
query,
matches: matches.map((k) => k.name),
count: matches.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Filter by metadata
app.get('/kv/filter/metadata', async (c) => {
const metadataKey = c.req.query('key');
const metadataValue = c.req.query('value');
if (!metadataKey) {
return c.json(
{
success: false,
error: 'Query parameter "key" is required',
},
400
);
}
try {
const matches: any[] = [];
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ cursor });
// Filter by metadata
const filteredKeys = result.keys.filter((key) => {
if (!key.metadata) return false;
return metadataValue
? key.metadata[metadataKey] === metadataValue
: metadataKey in key.metadata;
});
matches.push(...filteredKeys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
matches: matches.map((k) => ({
name: k.name,
metadata: k.metadata,
})),
count: matches.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cleanup & Maintenance
// ============================================================================
// Delete expired keys (manual cleanup)
app.post('/kv/cleanup/expired', async (c) => {
try {
let deletedCount = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ cursor });
// Filter keys that have expired
const now = Math.floor(Date.now() / 1000);
const expiredKeys = result.keys
.filter((key) => key.expiration && key.expiration < now)
.map((key) => key.name);
// Delete expired keys
await Promise.all(expiredKeys.map((key) => c.env.MY_KV.delete(key)));
deletedCount += expiredKeys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Deleted ${deletedCount} expired keys`,
deletedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Delete all keys with prefix (DANGEROUS!)
app.post('/kv/delete/prefix', async (c) => {
const { prefix } = await c.req.json<{ prefix: string }>();
if (!prefix) {
return c.json(
{
success: false,
error: 'Prefix is required',
},
400
);
}
try {
let deletedCount = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ prefix, cursor });
// Delete batch
await Promise.all(result.keys.map((key) => c.env.MY_KV.delete(key.name)));
deletedCount += result.keys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Deleted ${deletedCount} keys with prefix "${prefix}"`,
deletedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Namespace Statistics
// ============================================================================
// Get detailed namespace stats
app.get('/kv/stats/detailed', async (c) => {
try {
let totalKeys = 0;
let withMetadata = 0;
let withExpiration = 0;
const prefixes = new Map<string, number>();
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ cursor });
totalKeys += result.keys.length;
// Analyze keys
for (const key of result.keys) {
if (key.metadata) withMetadata++;
if (key.expiration) withExpiration++;
// Extract prefix (before first ":")
const prefix = key.name.split(':')[0];
prefixes.set(prefix, (prefixes.get(prefix) || 0) + 1);
}
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
stats: {
totalKeys,
withMetadata,
withExpiration,
prefixCounts: Object.fromEntries(prefixes),
},
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Group keys by prefix
app.get('/kv/groups', async (c) => {
try {
const groups = new Map<string, string[]>();
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ cursor });
for (const key of result.keys) {
const prefix = key.name.split(':')[0];
if (!groups.has(prefix)) {
groups.set(prefix, []);
}
groups.get(prefix)!.push(key.name);
}
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
groups: Object.fromEntries(groups),
groupCount: groups.size,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;

View File

@@ -0,0 +1,568 @@
/**
* Cloudflare Workers KV - Metadata Patterns
*
* This template demonstrates:
* - Storing data in metadata for list() efficiency
* - Metadata-based filtering
* - Versioning with metadata
* - Audit trails
* - Feature flags with metadata
*/
import { Hono } from 'hono';
type Bindings = {
MY_KV: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Metadata Optimization Pattern
// ============================================================================
/**
* Store small values in metadata to avoid separate get() calls
* Maximum metadata size: 1024 bytes (JSON serialized)
*/
// ❌ BAD: Requires 2 operations per key
async function getStatusBad(kv: KVNamespace, userId: string) {
const status = await kv.get(`user:${userId}:status`);
const lastSeen = await kv.get(`user:${userId}:lastseen`);
return { status, lastSeen };
}
// ✅ GOOD: Single list() operation gets metadata for all users
async function getStatusGood(kv: KVNamespace) {
const users = await kv.list({ prefix: 'user:' });
return users.keys.map((key) => ({
userId: key.name.split(':')[1],
status: key.metadata?.status,
lastSeen: key.metadata?.lastSeen,
}));
}
// Example: User status with metadata
app.post('/users/:id/status', async (c) => {
const userId = c.req.param('id');
const { status } = await c.req.json<{ status: string }>();
try {
// Store empty value, all data in metadata
await c.env.MY_KV.put(`user:${userId}`, '', {
metadata: {
status,
lastSeen: Date.now(),
plan: 'free',
},
});
return c.json({
success: true,
message: 'Status updated',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// List all users with status (no additional get() calls needed!)
app.get('/users/status', async (c) => {
try {
const users = await c.env.MY_KV.list({ prefix: 'user:' });
const statuses = users.keys.map((key) => ({
userId: key.name.split(':')[1],
status: key.metadata?.status || 'unknown',
lastSeen: key.metadata?.lastSeen,
plan: key.metadata?.plan,
}));
return c.json({
success: true,
users: statuses,
count: statuses.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Versioning with Metadata
// ============================================================================
interface VersionedData {
content: any;
version: number;
updatedAt: number;
updatedBy: string;
}
// Write with versioning
app.put('/config/:key', async (c) => {
const key = c.req.param('key');
const content = await c.req.json();
const updatedBy = c.req.header('X-User-ID') || 'system';
try {
// Get current version
const existing = await c.env.MY_KV.getWithMetadata<
VersionedData,
{ version: number }
>(`config:${key}`, { type: 'json' });
const currentVersion = existing.metadata?.version || 0;
const newVersion = currentVersion + 1;
// Store new version
const data: VersionedData = {
content,
version: newVersion,
updatedAt: Date.now(),
updatedBy,
};
await c.env.MY_KV.put(`config:${key}`, JSON.stringify(data), {
metadata: {
version: newVersion,
updatedAt: data.updatedAt,
updatedBy,
},
});
// Store version history (optional)
await c.env.MY_KV.put(
`config:${key}:v${newVersion}`,
JSON.stringify(data),
{
expirationTtl: 86400 * 30, // Keep versions for 30 days
}
);
return c.json({
success: true,
version: newVersion,
previousVersion: currentVersion,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Get config with version info
app.get('/config/:key', async (c) => {
const key = c.req.param('key');
try {
const { value, metadata } = await c.env.MY_KV.getWithMetadata<
VersionedData,
{ version: number; updatedAt: number; updatedBy: string }
>(`config:${key}`, { type: 'json' });
if (!value) {
return c.json(
{
success: false,
error: 'Config not found',
},
404
);
}
return c.json({
success: true,
data: value,
metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Get specific version
app.get('/config/:key/version/:version', async (c) => {
const key = c.req.param('key');
const version = c.req.param('version');
try {
const data = await c.env.MY_KV.get<VersionedData>(
`config:${key}:v${version}`,
{ type: 'json' }
);
if (!data) {
return c.json(
{
success: false,
error: `Version ${version} not found`,
},
404
);
}
return c.json({
success: true,
data,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Audit Trail with Metadata
// ============================================================================
interface AuditMetadata {
createdBy: string;
createdAt: number;
updatedBy: string;
updatedAt: number;
accessCount: number;
}
// Write with audit trail
app.post('/data/:key', async (c) => {
const key = c.req.param('key');
const value = await c.req.text();
const userId = c.req.header('X-User-ID') || 'anonymous';
try {
// Check if key exists
const existing = await c.env.MY_KV.getWithMetadata<string, AuditMetadata>(
key
);
const metadata: AuditMetadata = existing.metadata
? {
...existing.metadata,
updatedBy: userId,
updatedAt: Date.now(),
}
: {
createdBy: userId,
createdAt: Date.now(),
updatedBy: userId,
updatedAt: Date.now(),
accessCount: 0,
};
await c.env.MY_KV.put(key, value, { metadata });
return c.json({
success: true,
audit: metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read with access tracking
app.get('/data/:key', async (c) => {
const key = c.req.param('key');
try {
const { value, metadata } = await c.env.MY_KV.getWithMetadata<
string,
AuditMetadata
>(key);
if (!value) {
return c.json(
{
success: false,
error: 'Key not found',
},
404
);
}
// Increment access count (fire-and-forget)
if (metadata) {
c.executionCtx.waitUntil(
c.env.MY_KV.put(key, value, {
metadata: {
...metadata,
accessCount: metadata.accessCount + 1,
},
})
);
}
return c.json({
success: true,
value,
audit: metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Feature Flags with Metadata
// ============================================================================
interface FeatureFlag {
enabled: boolean;
rolloutPercentage: number;
targetUsers?: string[];
metadata: {
createdAt: number;
updatedAt: number;
description: string;
};
}
// Create feature flag
app.post('/flags/:name', async (c) => {
const name = c.req.param('name');
const flag = await c.req.json<FeatureFlag>();
try {
await c.env.MY_KV.put(`flag:${name}`, JSON.stringify(flag), {
metadata: {
enabled: flag.enabled,
rolloutPercentage: flag.rolloutPercentage,
updatedAt: Date.now(),
},
});
return c.json({
success: true,
message: `Feature flag "${name}" created`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// List all feature flags (metadata only)
app.get('/flags', async (c) => {
try {
const flags = await c.env.MY_KV.list({ prefix: 'flag:' });
const flagList = flags.keys.map((key) => ({
name: key.name.replace('flag:', ''),
enabled: key.metadata?.enabled || false,
rolloutPercentage: key.metadata?.rolloutPercentage || 0,
updatedAt: key.metadata?.updatedAt,
}));
return c.json({
success: true,
flags: flagList,
count: flagList.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Check feature flag for user
app.get('/flags/:name/check/:userId', async (c) => {
const name = c.req.param('name');
const userId = c.req.param('userId');
try {
const flag = await c.env.MY_KV.get<FeatureFlag>(`flag:${name}`, {
type: 'json',
});
if (!flag) {
return c.json(
{
success: false,
error: 'Feature flag not found',
},
404
);
}
// Check if enabled
if (!flag.enabled) {
return c.json({ enabled: false, reason: 'Flag disabled globally' });
}
// Check target users
if (flag.targetUsers && flag.targetUsers.length > 0) {
const enabled = flag.targetUsers.includes(userId);
return c.json({
enabled,
reason: enabled ? 'User in target list' : 'User not in target list',
});
}
// Check rollout percentage
const userHash =
parseInt(userId.split('').reduce((a, b) => a + b.charCodeAt(0), 0).toString()) %
100;
const enabled = userHash < flag.rolloutPercentage;
return c.json({
enabled,
reason: enabled
? `User in ${flag.rolloutPercentage}% rollout`
: `User not in ${flag.rolloutPercentage}% rollout`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Metadata Size Validation
// ============================================================================
// Validate metadata size before writing
app.post('/validate/metadata', async (c) => {
const { metadata } = await c.req.json<{ metadata: any }>();
const serialized = JSON.stringify(metadata);
const size = new TextEncoder().encode(serialized).length;
if (size > 1024) {
return c.json({
valid: false,
size,
maxSize: 1024,
error: `Metadata too large: ${size} bytes (max 1024)`,
});
}
return c.json({
valid: true,
size,
maxSize: 1024,
});
});
// ============================================================================
// Metadata Migration
// ============================================================================
// Migrate existing keys to add metadata
app.post('/migrate/add-metadata', async (c) => {
const { prefix, metadata } = await c.req.json<{
prefix: string;
metadata: any;
}>();
try {
let migratedCount = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ prefix, cursor });
// Migrate batch
for (const key of result.keys) {
// Get existing value
const value = await c.env.MY_KV.get(key.name);
if (value !== null) {
// Re-write with metadata
await c.env.MY_KV.put(key.name, value, {
metadata: {
...key.metadata,
...metadata,
migratedAt: Date.now(),
},
});
migratedCount++;
}
}
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Migrated ${migratedCount} keys`,
migratedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;

View File

@@ -0,0 +1,56 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"observability": {
"enabled": true
},
// KV Namespace Bindings
"kv_namespaces": [
{
// The binding name - accessible as env.CACHE in your Worker
"binding": "CACHE",
// Production namespace ID (from: wrangler kv namespace create CACHE)
"id": "<YOUR_PRODUCTION_NAMESPACE_ID>",
// Preview/local namespace ID (from: wrangler kv namespace create CACHE --preview)
// This is optional but recommended for local development
"preview_id": "<YOUR_PREVIEW_NAMESPACE_ID>"
},
// Multiple namespaces example
{
"binding": "CONFIG",
"id": "<CONFIG_PRODUCTION_ID>",
"preview_id": "<CONFIG_PREVIEW_ID>"
},
{
"binding": "SESSIONS",
"id": "<SESSIONS_PRODUCTION_ID>",
"preview_id": "<SESSIONS_PREVIEW_ID>"
}
]
// IMPORTANT NOTES:
//
// 1. Create namespaces first:
// npx wrangler kv namespace create CACHE
// npx wrangler kv namespace create CACHE --preview
//
// 2. Copy the IDs from the command output to this file
//
// 3. NEVER commit real namespace IDs to public repos
// Use environment variables for sensitive namespaces:
// "id": "${KV_CACHE_ID}"
//
// 4. preview_id is optional but recommended for local development
// It creates a separate namespace for testing
//
// 5. Binding names must be valid JavaScript identifiers
// Good: CACHE, MY_KV, UserData
// Bad: my-kv, user.data, 123kv
}