DEV Community

Cover image for Stop Using Databases for Small Static Sites — Use Cloudflare Workers + KV Instead
Franklin-hyriol
Franklin-hyriol

Posted on

Stop Using Databases for Small Static Sites — Use Cloudflare Workers + KV Instead

Static sites don’t need heavyweight backends

There’s a reflex most developers have:
the moment data needs to be stored, a database enters the conversation.

PostgreSQL. MongoDB. Prisma. ORM. Hosting. Billing.

And for small static sites, this reflex is often wrong.

If your needs are simple — storing a bit of JSON, sharing state via URLs, persisting lightweight data — spinning up a “real backend” is unnecessary complexity.

This article shows a **simpler, cheaper, and cleaner alternative **using Cloudflare Workers and KV.

The real-world use case

I’m building a small collection of online tools hosted as a fully static site on Cloudflare Pages.

One of those tools is a Random Wheel.
I wanted users to be able to:

  • save a list of values
  • generate a short shareable URL
  • reload the wheel later from that link

That’s it.
No auth. No relations. No queries. Just JSON in, JSON out.

Why a traditional database makes no sense here

Adding a real database would have meant:

  • maintaining a backend
  • handling deployment and secrets
  • paying for infra that stays mostly idle
  • increasing failure points

All of that just to store small arrays of strings.

This is exactly the kind of problem serverless edge tools were designed for.

The alternative: Workers + KV

The solution is a simple combination:

  • Cloudflare Worker → acts as a tiny API
  • KV Namespace → stores key-value data globally

What you get:

  • no server to manage
  • data available at the edge
  • extremely low latency
  • generous free tier
  • perfect fit for static sites

You’re not replacing databases everywhere.
You’re not using them where they don’t belong.

Architecture overview (simple on purpose)

The flow looks like this:

  1. Browser sends data to a Worker
  2. Worker generates a short ID
  3. Data is stored in KV under that ID
  4. Later requests fetch the data using the same ID

Create a KV namespace

Using Wrangler (Cloudflare’s CLI):

   npx wrangler kv:namespace create "APP_KV"
Enter fullscreen mode Exit fullscreen mode

This creates a globally replicated key-value store you can bind to your Worker.

Bind KV in wrangler.toml

   name = "static-site-api" # The name of your worker
   main = "worker/index.ts" # The path to your worker's entry point
   compatibility_date = "2024-05-12"
   compatibility_flags = ["nodejs_compat"]

   # KV Namespace binding
   [[kv_namespaces]]
   binding = "APP_KV"
   id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Enter fullscreen mode Exit fullscreen mode

The binding name becomes important — it’s how your Worker accesses storage.

Install Worker dependencies

Inside a worker/ folder:

   {
     "name": "static-site-worker",
     "private": true,
     "scripts": {
       "dev": "wrangler dev",
       "deploy": "wrangler deploy"
     },
     "dependencies": {
       "nanoid": "^5.0.7"
     },
     "devDependencies": {
       "@cloudflare/workers-types": "^4.20240512.0",
       "wrangler": "^3.57.0"
     }
   }
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • nanoid → short, collision-resistant IDs
  • Workers types → clean TypeScript support
  • Wrangler → deploy & test easily

Write the Worker API

A minimal API with two endpoints:

  • POST → store data
  • GET → retrieve data
   /// <reference types="@cloudflare/workers-types" />
import { nanoid } from 'nanoid';

export interface Env {
  WHEEL_KV: KVNamespace;
}

const worker = {
  async fetch(request: Request, env: Env): Promise<Response> {
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*', // restrict in production
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    };

    if (request.method === 'OPTIONS') {
      return new Response(null, { headers: corsHeaders });
    }

    const url = new URL(request.url);

    try {
      switch (request.method) {
        case 'POST': {
          const options = await request.json<string[]>();

          if (!Array.isArray(options) || options.length === 0) {
            return new Response('Invalid payload', { status: 400 });
          }

          const id = nanoid(8);
          await env.WHEEL_KV.put(id, JSON.stringify(options));

          return new Response(JSON.stringify({ success: true, id }), {
            status: 201,
            headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          });
        }

        case 'GET': {
          const id = url.searchParams.get('id');
          if (!id) {
            return new Response('Missing id', { status: 400 });
          }

          const data = await env.WHEEL_KV.get(id);
          if (!data) {
            return new Response('Not found', { status: 404 });
          }

          return new Response(data, {
            status: 200,
            headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          });
        }

        default:
          return new Response('Method not allowed', { status: 405 });
      }
    } catch {
      return new Response('Server error', { status: 500 });
    }
  },
};

export default worker;
Enter fullscreen mode Exit fullscreen mode

Deploy it

From the root directory:

   npx wrangler deploy
Enter fullscreen mode Exit fullscreen mode

You’ll get a public URL like:

   https://static-site-api.your-name.workers.dev
Enter fullscreen mode Exit fullscreen mode

That URL is your backend.

Call it from your static frontend

From React / Next.js / vanilla JS:

const handleShare = async () => {
  const options = ['Pizza', 'Burger', 'Tacos'];

  const response = await fetch('https://static-site-api.your-name.workers.dev', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(options),
  });

  const result = await response.json();

  if (result.success) {
    const shareableUrl = `https://yoursite.com/tool?id=${id}`;
    navigator.clipboard.writeText(shareableUrl);
  }
};
Enter fullscreen mode Exit fullscreen mode

When this approach makes sense

Use this if:

  • your site is static
  • data is small and simple
  • access patterns are predictable
  • you don’t need complex queries

Do not use this if:

  • you need strong consistency
  • you’re building relational data
  • writes are extremely frequent
  • you need transactional guarantees

Top comments (0)