mcpbeat

Bun Cloudflare Workers

secondsky/bun-cloudflare-workers

This skill should be used when the user asks about "Cloudflare Workers with Bun", "deploying Bun to Workers", "wrangler with Bun", "edge deployment", "Bun to Cloudflare", or building and deploying applications to Cloudflare Workers using Bun.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
202
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/secondsky/claude-skills --skill bun-cloudflare-workers

The instruction itself

22 sections, as written by the author

Bun Cloudflare Workers

Build and deploy Cloudflare Workers using Bun for development.

Quick Start

# Create new Workers project
bunx create-cloudflare my-worker
cd my-worker

# Install dependencies
bun install

# Development
bun run dev

# Deploy
bun run deploy

Secure Installation

Scaffolding tools like bunx create-cloudflare download and execute remote code. Before running, follow supply chain security best practices:

  • Block post-install scripts — Bun disables them by default; allow specific packages via trustedDependencies in package.json
  • Cooldown period — Configure minimumReleaseAge in bunfig.toml to wait 7 days for new versions
  • Audit before installing — Run socket package score npm <pkg> or use socket npm install <pkg> to check packages

Load the dependency-upgrade skill for full security configuration including Socket CLI integration, cooldown setup, lockfile validation, and CI enforcement.

Project Setup

package.json

{
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy",
    "build": "bun build src/index.ts --outdir=dist --target=browser"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20250906.0",
    "wrangler": "^4.54.0"
  }
}

wrangler.toml

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"

# Use Bun for local dev
[dev]
local_protocol = "http"

# Bindings
[[kv_namespaces]]
binding = "KV"
id = "xxx"

[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxx"

Basic Worker

// src/index.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/") {
      return new Response("Hello from Cloudflare Workers!");
    }

    if (url.pathname === "/api/data") {
      return Response.json({ message: "Hello" });
    }

    return new Response("Not Found", { status: 404 });
  },
};

interface Env {
  KV: KVNamespace;
  DB: D1Database;
}

Using Hono

// src/index.ts
import { Hono } from "hono";

type Bindings = {
  KV: KVNamespace;
  DB: D1Database;
};

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

app.get("/", (c) => c.text("Hello Hono!"));

app.get("/api/users", async (c) => {
  const users = await c.env.DB.prepare("SELECT * FROM users").all();
  return c.json(users.results);
});

app.post("/api/users", async (c) => {
  const { name } = await c.req.json();
  await c.env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind(name).run();
  return c.json({ success: true });
});

export default app;

KV Storage

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const key = url.searchParams.get("key");

    if (request.method === "GET" && key) {
      const value = await env.KV.get(key);
      return Response.json({ key, value });
    }

    if (request.method === "PUT" && key) {
      const value = await request.text();
      await env.KV.put(key, value, { expirationTtl: 3600 });
      return Response.json({ success: true });
    }

    return new Response("Bad Request", { status: 400 });
  },
};

D1 Database

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Query
    const { results } = await env.DB.prepare(
      "SELECT * FROM users WHERE active = ?"
    ).bind(1).all();

    // Insert
    const info = await env.DB.prepare(
      "INSERT INTO users (name, email) VALUES (?, ?)"
    ).bind("Alice", "[email protected]").run();

    // Transaction
    const batch = await env.DB.batch([
      env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Bob"),
      env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Charlie"),
    ]);

    return Response.json(results);
  },
};

Durable Objects

// src/counter.ts
export class Counter {
  private state: DurableObjectState;
  private value = 0;

  constructor(state: DurableObjectState) {
    this.state = state;
    this.state.blockConcurrencyWhile(async () => {
      this.value = (await this.state.storage.get("value")) || 0;
    });
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/increment") {
      this.value++;
      await this.state.storage.put("value", this.value);
    }

    return Response.json({ value: this.value });
  }
}

// src/index.ts
export { Counter } from "./counter";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const id = env.COUNTER.idFromName("global");
    const stub = env.COUNTER.get(id);
    return stub.fetch(request);
  },
};
# wrangler.toml
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"

[[migrations]]
tag = "v1"
new_classes = ["Counter"]

R2 Storage

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);

    if (request.method === "GET") {
      const object = await env.BUCKET.get(key);
      if (!object) {
        return new Response("Not Found", { status: 404 });
      }
      return new Response(object.body, {
        headers: { "Content-Type": object.httpMetadata?.contentType || "application/octet-stream" },
      });
    }

    if (request.method === "PUT") {
      await env.BUCKET.put(key, request.body, {
        httpMetadata: { contentType: request.headers.get("Content-Type") || undefined },
      });
      return Response.json({ success: true });
    }

    return new Response("Method Not Allowed", { status: 405 });
  },
};

Development with Bun

Local Development

# Run with wrangler (uses Bun for TypeScript)
bun run dev

# Or directly
bunx wrangler dev

Testing with Bun

// src/index.test.ts
import { describe, test, expect } from "bun:test";

// Mock worker
const worker = {
  async fetch(request: Request) {
    return new Response("Hello");
  },
};

describe("Worker", () => {
  test("returns hello", async () => {
    const request = new Request("http://localhost/");
    const response = await worker.fetch(request);
    expect(await response.text()).toBe("Hello");
  });
});

Miniflare for Testing

import { Miniflare } from "miniflare";

const mf = new Miniflare({
  script: await Bun.file("./dist/index.js").text(),
  kvNamespaces: ["KV"],
});

const response = await mf.dispatchFetch("http://localhost/");
console.log(await response.text());

Build for Production

// build.ts
await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  target: "browser", // Workers use browser APIs
  minify: true,
  sourcemap: "external",
});
bun run build.ts
bunx wrangler deploy

Environment Variables

# wrangler.toml
[vars]
API_URL = "https://api.example.com"

# Secrets (set via CLI)
# wrangler secret put API_KEY
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    console.log(env.API_URL);    // From vars (non-secret, safe to log)
    // Access a Worker secret (NEVER log it — wrangler tail / Logpush persist logs)
    if (env.API_KEY) {
      // use env.API_KEY to call the upstream API
    }
    // ❌ NEVER: console.log(env.API_KEY) — secrets must not appear in logs
    return new Response("OK");
  },
};

Scheduled Workers (Cron)

export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
    console.log("Cron triggered at:", event.scheduledTime);
    // Perform scheduled task
    await env.DB.prepare("DELETE FROM logs WHERE created_at < ?")
      .bind(Date.now() - 7 * 24 * 60 * 60 * 1000)
      .run();
  },

  async fetch(request: Request, env: Env): Promise<Response> {
    return new Response("OK");
  },
};
# wrangler.toml
[triggers]
crons = ["0 * * * *"]  # Every hour

Common Errors

| Error | Cause | Fix |

|-------|-------|-----|

| Bun API not available | Workers use V8 | Use Web APIs only |

| Module not found | Build issue | Check bundler config |

| Script too large | Exceeds 10MB | Optimize bundle |

| CPU time exceeded | Long execution | Optimize or use queues |

API Compatibility

Workers support Web APIs, NOT Bun-specific APIs:

| Available | Not Available |

|-----------|---------------|

| fetch() | Bun.file() |

| Response | Bun.serve() |

| Request | bun:sqlite |

| URL | bun:ffi |

| crypto | fs |

| TextEncoder | child_process |

When to Load References

Load references/bindings.md when:

  • Advanced KV/D1/R2 patterns
  • Queue workers
  • Service bindings

Load references/performance.md when:

  • Bundle optimization
  • Cold start reduction
  • Caching strategies

How to use it

Copy the folder

Take secondsky/bun-cloudflare-workers from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npm. Without those the skill loads but fails at the first command.