DEV Community

Cover image for I Built a Zero-Dependency KV Store That Works in Browser, Node.js AND Cloudflare Workers
Prince Kumar
Prince Kumar

Posted on

I Built a Zero-Dependency KV Store That Works in Browser, Node.js AND Cloudflare Workers

The Problem I Kept Running Into

Every time I start a new project, I write the same boilerplate. Need to cache something in the browser? LocalStorage. On the server? Write a Map wrapper. On Cloudflare Workers? Completely different API again.

// Browser
const get = (key) => {
  if (typeof window === 'undefined') return null;
  const item = localStorage.getItem(key);
  return item ? JSON.parse(item) : null;
};

// Node.js — completely different
const cache = new Map();
const get = (key) => cache.get(key) ?? null;

// Cloudflare Workers — different again
const get = async (key) => {
  const val = await env.MY_KV.get(key);
  return val ? JSON.parse(val) : null;
};
Enter fullscreen mode Exit fullscreen mode

Three runtimes. Three different APIs. Zero TTL support. Zero type safety. And I haven't even added error handling yet.

I got tired of this. So I built kv-one

One API. Every Runtime.

//ts
import { kv } from 'kv-one';

// Works identically in Browser, Node.js, and Cloudflare Workers
await kv.set('user', { name: 'Alice', role: 'admin' });
const user = await kv.get<{ name: string; role: string }>('user');

Enter fullscreen mode Exit fullscreen mode

That's it. No configuration. The library detects your runtime automatically and picks the right storage backend.

What Makes It Different

I checked the existing packages before building this. Most of them have one or more of these problems:

  • Synchronous API : blocks the event loop, terrible for servers.

  • No TTL support : you can't expire data automatically

  • No Cloudflare Workers support : the fastest growing edge runtime is just... missing

  • Runtime dependencies : adding idb-keyval, sqlite, etc. to your bundle

  • Silent error swallowing : failures disappear, debugging becomes a nightmare
    **kv-one **solves all of these.

Key Features

✅ Fully Async — Never Blocks the Event Loop


// ts
// All operations are Promise-based
const value = await kv.get('key');
await kv.set('key', value);
await kv.delete('key');


Enter fullscreen mode Exit fullscreen mode

✅ TTL — Built-in Automatic Expiry

//ts
// Expires in 1 hour — works on ALL runtimes
await kv.set('otp', '482910', { ttl: 3600 });

// Returns null after expiry — automatically
const otp = await kv.get('otp');

Enter fullscreen mode Exit fullscreen mode

TTL is implemented at the library layer — not delegated to native storage. So it works uniformly whether you're on localStorage, Memory, or Cloudflare KV.

✅ Namespacing — Perfect for Multi-Tenant Apps

//ts
const userStore = kv.namespace('users');
const sessionStore = kv.namespace('sessions');

await userStore.set('alice', { role: 'admin' });
await sessionStore.set('alice', { token: 'abc123' });

// Stored as 'users:alice' and 'sessions:alice' — no collisions
const keys = await userStore.keys(); // ['alice'] — scoped!
Enter fullscreen mode Exit fullscreen mode

✅ Cloudflare Workers — First-Class Support

//ts
// workers.ts
import { createKV, CloudflareAdapter } from 'kv-one';

export default {
  async fetch(request: Request, env: Env) {
    const store = createKV({
      adapter: new CloudflareAdapter({ namespace: env.MY_KV })
    });

    await store.set('visits', count, { ttl: 86400 });
    const visits = await store.get<number>('visits');
  }
};
Enter fullscreen mode Exit fullscreen mode

✅ React Hook — Reactive KV State

//ts
import { useKV } from 'kv-one/react';

function ThemeToggle() {
  const { value: theme, setValue, loading } = useKV<string>('theme', 'dark');

  if (loading) return <span>Loading...</span>;

  return (
    <button onClick={() => setValue(theme === 'dark' ? 'light' : 'dark')}>
      Current theme: {theme}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

SSR-safe. Returns **defaultValue **on the server without touching the adapter.

✅ Zero Runtime Dependencies

npm i kv-one
Enter fullscreen mode Exit fullscreen mode

That's it. Nothing else is installed. No idb-keyval, no better-sqlite3, no bloat. 31 KB total package size.

✅ Typed Error Classes

//ts
import { KVError, TTLError, KeyValidationError, AdapterError } from 'kv-one';

try {
  await kv.set('my key', value); // space in key name
} catch (err) {
  if (err instanceof KeyValidationError) {
    console.log('Invalid key:', err.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

No more debugging silent failures.
Or override it explicitly for full control:

//ts
import { createKV, MemoryAdapter, LocalStorageAdapter } from 'kv-one';

// Force in-memory (great for tests)``
const testStore = createKV({ adapter: new MemoryAdapter() });

// Force localStorage with a namespace prefix
const appStore = createKV({
  adapter: new LocalStorageAdapter(),
  prefix: 'myapp'
});
Enter fullscreen mode Exit fullscreen mode

Quick Start

npm install kv-one
Enter fullscreen mode Exit fullscreen mode
//ts
import { kv } from 'kv-one';

// Basic usage
await kv.set('counter', 0);
const count = await kv.get<number>('counter');
await kv.delete('counter');

// With TTL (seconds)
await kv.set('session', token, { ttl: 1800 }); // 30 minutes

// Namespaced
const cache = kv.namespace('cache');
await cache.set('posts', posts, { ttl: 300 });
await cache.clear(); // only clears cache:* keys

// Check existence
const exists = await kv.exists('session');

// List all keys
const keys = await kv.keys(); // ['session']
Enter fullscreen mode Exit fullscreen mode

Security Built In
Security was a primary concern from the start:

  • Prototype pollution blocked: custom JSON reviver strips proto, constructor, prototypekeys

  • Circular reference detection: serializer catches them before they crash your app

  • Key validation: rejects keys with spaces, control characters, or dangerous patterns

  • Storage quota errors: surfaced as typed **AdapterError **instead of silent failures

📦 npm: npmjs.com/package/kv-one
🐙 GitHub: github.com/kumarprincepk/kv-one

If this saves you from writing the same localStorage boilerplate one more time, leave a ⭐ on GitHub — it really helps!

What runtimes are you targeting in 2026? Drop a comment below 👇

Top comments (0)