Claude Code for Cloudflare KV: Edge Key-Value Storage — Claude Skills 360 Blog
Blog / Backend / Claude Code for Cloudflare KV: Edge Key-Value Storage
Backend

Claude Code for Cloudflare KV: Edge Key-Value Storage

Published: April 1, 2027
Read time: 7 min read
By: Claude Skills 360

Cloudflare KV is a globally distributed key-value store for Workers and Pages — env.KV_NAMESPACE.get(key) reads a value, env.KV_NAMESPACE.put(key, value, { expirationTtl: 3600 }) writes with TTL. Values are strings by default; { type: "json" } on get auto-parses JSON. list({ prefix, limit, cursor }) paginates through keys. KV is eventually consistent — reads may be stale by up to 60 seconds globally. Bindings are declared in wrangler.toml and are available on the env object in Worker handlers. In Hono, c.env.KV accesses the binding. In Next.js on Cloudflare, getRequestContext().env.KV retrieves it inside Server Components and Route Handlers via @cloudflare/next-on-pages. Claude Code generates KV binding configs, JSON storage helpers, TTL-based caching wrappers, rate limiting patterns, and feature flag storage for Cloudflare Workers.

CLAUDE.md for Cloudflare KV

## Cloudflare KV Stack
- Version: wrangler >= 3.60, @cloudflare/workers-types >= 4.20
- Binding: [[kv_namespaces]] name = "KV" id = "..." in wrangler.toml
- Read: const val = await env.KV.get("key") — null if missing
- Write: await env.KV.put("key", JSON.stringify(data), { expirationTtl: 3600 })
- JSON: await env.KV.get("key", { type: "json" }) — parse automatically
- List: await env.KV.list({ prefix: "user:", limit: 100, cursor })
- Delete: await env.KV.delete("key")
- Next.js: import { getRequestContext } from "@cloudflare/next-on-pages"
- Consistency: eventually consistent ~60s — not suitable for counters (use Durable Objects)

Wrangler Configuration

# wrangler.toml — KV namespace bindings
name = "my-worker"
main = "src/worker.ts"
compatibility_date = "2024-01-01"

# Production KV namespace
[[kv_namespaces]]
binding = "KV"
id = "abc123..."

# Preview KV namespace for local dev
[[kv_namespaces]]
binding = "KV"
id = "def456..."
preview_id = "def456..."

# TypeScript type additions — src/worker-env.d.ts
# interface CloudflareEnv {
#   KV: KVNamespace
# }

KV Storage Helpers

// lib/kv-storage.ts — typed KV utilities
import type { KVNamespace } from "@cloudflare/workers-types"

// Generic typed get/put helpers
export async function kvGet<T>(kv: KVNamespace, key: string): Promise<T | null> {
  return kv.get<T>(key, { type: "json" })
}

export async function kvPut<T>(
  kv: KVNamespace,
  key: string,
  value: T,
  options: { ttl?: number; expirationDate?: Date } = {}
): Promise<void> {
  const putOptions: KVNamespacePutOptions = {}
  if (options.ttl) putOptions.expirationTtl = options.ttl
  if (options.expirationDate) putOptions.expiration = Math.floor(options.expirationDate.getTime() / 1000)
  await kv.put(key, JSON.stringify(value), putOptions)
}

export async function kvDelete(kv: KVNamespace, key: string): Promise<void> {
  await kv.delete(key)
}

// List all keys with a prefix (handles pagination)
export async function kvListAll(
  kv: KVNamespace,
  prefix: string,
  limit = 1000
): Promise<string[]> {
  const keys: string[] = []
  let cursor: string | undefined

  while (true) {
    const result = await kv.list({ prefix, limit: Math.min(limit - keys.length, 1000), cursor })
    keys.push(...result.keys.map(k => k.name))
    if (result.list_complete || keys.length >= limit) break
    cursor = result.cursor
  }

  return keys
}

// Cache wrapper — read from KV, fallback to fetch, store result
export async function withCache<T>(
  kv: KVNamespace,
  cacheKey: string,
  fetcher: () => Promise<T>,
  ttlSeconds = 300
): Promise<T> {
  const cached = await kvGet<T>(kv, cacheKey)
  if (cached !== null) return cached

  const fresh = await fetcher()
  await kvPut(kv, cacheKey, fresh, { ttl: ttlSeconds })
  return fresh
}

Hono Worker with KV

// src/worker.ts — Hono app with KV bindings
import { Hono } from "hono"
import { cache } from "hono/cache"
import { kvGet, kvPut, withCache } from "./lib/kv-storage"

interface Env {
  KV: KVNamespace
}

const app = new Hono<{ Bindings: Env }>()

// Feature flags stored in KV
interface FeatureFlags {
  newCheckout: boolean
  experimentalSearch: boolean
  maintenanceMode: boolean
}

const DEFAULT_FLAGS: FeatureFlags = {
  newCheckout: false,
  experimentalSearch: false,
  maintenanceMode: false,
}

app.get("/api/flags", async c => {
  const flags = await withCache<FeatureFlags>(
    c.env.KV,
    "feature-flags",
    async () => {
      const stored = await kvGet<FeatureFlags>(c.env.KV, "feature-flags")
      return stored ?? DEFAULT_FLAGS
    },
    60  // Cache for 60s — flags change infrequently
  )
  return c.json(flags)
})

app.put("/api/flags", async c => {
  const updates = await c.req.json<Partial<FeatureFlags>>()
  const current = await kvGet<FeatureFlags>(c.env.KV, "feature-flags") ?? DEFAULT_FLAGS
  const updated = { ...current, ...updates }
  await kvPut(c.env.KV, "feature-flags", updated)
  // Invalidate cache
  await c.env.KV.delete("feature-flags:cache")
  return c.json(updated)
})

// Session storage in KV
interface Session {
  userId: string
  role: string
  createdAt: string
}

app.get("/api/session/:token", async c => {
  const session = await kvGet<Session>(c.env.KV, `session:${c.req.param("token")}`)
  if (!session) return c.json({ error: "Session not found" }, 404)
  return c.json(session)
})

app.post("/api/session", async c => {
  const body = await c.req.json<{ userId: string; role: string }>()
  const token = crypto.randomUUID()
  const session: Session = {
    userId: body.userId,
    role: body.role,
    createdAt: new Date().toISOString(),
  }
  // Sessions expire after 7 days
  await kvPut(c.env.KV, `session:${token}`, session, { ttl: 7 * 24 * 3600 })
  return c.json({ token })
})

export default app

Rate Limiting with KV

// lib/rate-limiter.ts — sliding window rate limiting
import type { KVNamespace } from "@cloudflare/workers-types"

interface RateLimitResult {
  allowed: boolean
  remaining: number
  resetAt: number
}

// Simple fixed-window rate limiter using KV
// Note: KV is eventually consistent — use Durable Objects for strict rate limiting
export async function checkRateLimit(
  kv: KVNamespace,
  identifier: string,  // e.g., IP address or user ID
  {
    limit = 100,
    windowSeconds = 60,
  }: { limit?: number; windowSeconds?: number } = {}
): Promise<RateLimitResult> {
  const windowStart = Math.floor(Date.now() / (windowSeconds * 1000))
  const key = `rate:${identifier}:${windowStart}`

  const current = await kv.get(key) ?? "0"
  const count = parseInt(current, 10)

  if (count >= limit) {
    return {
      allowed: false,
      remaining: 0,
      resetAt: (windowStart + 1) * windowSeconds * 1000,
    }
  }

  // Increment counter
  await kv.put(key, String(count + 1), { expirationTtl: windowSeconds * 2 })

  return {
    allowed: true,
    remaining: limit - count - 1,
    resetAt: (windowStart + 1) * windowSeconds * 1000,
  }
}

Next.js on Cloudflare Pages

// app/api/flags/route.ts — KV in Next.js Route Handler
import { getRequestContext } from "@cloudflare/next-on-pages"

export const runtime = "edge"

export async function GET() {
  const { env } = getRequestContext()
  const flags = await env.KV.get("feature-flags", { type: "json" })
  return Response.json(flags ?? {})
}

// middleware.ts — KV access in Edge Middleware
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { getRequestContext } from "@cloudflare/next-on-pages"

export const runtime = "experimental-edge"

export async function middleware(request: NextRequest) {
  const { env } = getRequestContext()

  // Check maintenance mode from KV
  const maintenance = await env.KV.get("maintenance-mode")
  if (maintenance === "true" && !request.nextUrl.pathname.startsWith("/maintenance")) {
    return NextResponse.redirect(new URL("/maintenance", request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
}

For the Cloudflare Durable Objects alternative when strongly consistent counters, atomic operations, or WebSocket session state persisted at the edge are needed — Durable Objects guarantee single-writer consistency that KV cannot provide, essential for rate limiting, reservation systems, and real-time collaboration state, see the Durable Objects guide. For the Redis/Upstash alternative when rich data structures (sorted sets, streams, pub/sub), atomic Lua scripts, or compatibility with existing Redis tooling are needed from a serverless-compatible store — Upstash provides Redis API compatibility over HTTP for edge environments, see the serverless Redis guide. The Claude Skills 360 bundle includes Cloudflare KV skill sets covering caching, session storage, and feature flags. Start with the free tier to try edge storage generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free