This site is not affiliated with or endorsed by Cloudflare, Inc. It simply showcases experiments built using Cloudflare services.
Cloudflare Experiments

Adding New Experiments

Step-by-step guide to creating and adding new experiments to Cloudflare Experiments

This guide walks you through the process of adding a new experiment to the Cloudflare Experiments repository.

Before You Start

Always open an issue using the Experiment idea template to discuss your experiment with maintainers before implementing it.

What Makes a Good Experiment?

A good experiment:

  • Demonstrates a specific Cloudflare capability (Workers AI, Browser Rendering, D1, KV, R2, etc.)
  • Solves a real developer need (not just "Hello World")
  • Is edge-first and runs in under 60 seconds
  • Has a single responsibility (one experiment = one capability)
  • Is independently deployable without dependencies on other experiments
  • Provides practical value that developers would actually use

Step-by-Step Process

Open an experiment idea issue

Use the Experiment idea template to propose your experiment. Include:

  • What Cloudflare capability it demonstrates
  • What problem it solves
  • Basic description of how it works
  • Any special bindings or resources needed (D1, KV, AI, etc.)

Wait for maintainer feedback before proceeding.

Create the experiment directory

Create a new directory under apps/experiments/<name>/:

mkdir -p apps/experiments/my-experiment
cd apps/experiments/my-experiment

Use kebab-case for the experiment name (e.g. is-it-down, link-shortener).

Set up the project structure

Create the standard directory structure:

index.ts
package.json
wrangler.json
tsconfig.json
README.md

Initialize package.json

Create a package.json with the required dependencies:

{
  "name": "my-experiment",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy"
  },
  "dependencies": {
    "hono": "^4.0.0"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.0.0",
    "typescript": "^5.0.0",
    "wrangler": "^3.0.0"
  }
}

Then install dependencies:

npm install

Create wrangler.toml

Configure your Worker with wrangler.toml:

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

# Add bindings as needed:
# [[d1_databases]]
# binding = "DB"
# database_name = "my-database"
# database_id = "..."

# [[kv_namespaces]]
# binding = "MY_KV"
# id = "..."

Or use wrangler.json if you prefer JSON format.

Set up TypeScript configuration

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "lib": ["ES2022"],
    "types": ["@cloudflare/workers-types"],
    "strict": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Implement the Worker

Follow the code standards to implement your Worker:

Create src/types/env.d.ts:

/// <reference types="@cloudflare/workers-types" />

export interface Env {
  // Add your bindings here
  // DB: D1Database;
  // MY_KV: KVNamespace;
}

Create src/utils/response.ts:

import type { Context } from "hono";

export function jsonError(c: Context, message: string, code: string, status: 400 | 404 | 502 = 400) {
  return c.json({ error: message, code }, { status });
}

export function jsonSuccess<T>(c: Context, data: T, status: 200 = 200) {
  return c.json(data, { status });
}

Create src/lib/url.ts (if handling URL params):

/**
 * Validates and normalizes a URL for safe fetching.
 * Returns the URL string or null if invalid/not allowed.
 */
export function validateUrl(input: string | undefined): string | null {
  if (!input || typeof input !== "string") return null;
  const trimmed = input.trim();
  if (!trimmed) return null;
  try {
    const url = new URL(trimmed);
    if (url.protocol !== "http:" && url.protocol !== "https:") {
      return null;
    }
    return url.href;
  } catch {
    return null;
  }
}

Create your route handlers in src/routes/:

import { Hono } from "hono";
import type { Env } from "../types/env";
import { jsonError, jsonSuccess } from "../utils/response";

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

app.get("/my-route", async (c) => {
  // Your logic here
  return jsonSuccess(c, { message: "Success" });
});

export default app;

Create src/index.ts:

import { Hono } from "hono";
import type { Env } from "./types/env";
import myRoutes from "./routes/my-route";

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

app.route("/", myRoutes);

app.get("/", (c) => {
  return c.json({
    name: "my-experiment",
    description: "What this experiment does",
    usage: "GET /my-route",
  });
});

app.onError((err, c) => {
  return c.json({ error: err.message, code: "INTERNAL_ERROR" }, 500);
});

export default {
  fetch: app.fetch,
};

Test locally

Run your experiment locally:

npm run dev

Test your endpoints:

curl http://localhost:8787/my-route

Verify:

  • All routes work as expected
  • Error handling works correctly
  • TypeScript has no errors

Create README.md

Document your experiment with a comprehensive README:

# My Experiment

Brief description of what this experiment does.

## Features

- Feature 1
- Feature 2

## API

### `GET /my-route`

| Query | Required | Description |
| ----- | -------- | ----------- |
| `param` | Yes | Description |

**Example**

```http
GET /my-route?param=value

Response

{
  "result": "data"
}

Errors

  • 400 - Invalid parameter

Run locally

cd apps/experiments/my-experiment
npm install
npm run dev

Deploy

Deploy to Cloudflare Workers

Cloudflare features used

  • Workers
  • List relevant features

</Step>

<Step>

### Add to root README.md

Add a row to the experiments table in the root `README.md`:
```markdown
| [My Experiment](apps/experiments/my-experiment/) | Brief description | [Deploy](https://deploy.workers.cloudflare.com/?url=https://github.com/YOUR_USERNAME/cloudflare-experiments/tree/main/apps/experiments/my-experiment) |

Document on the docs site

Generate the Fumadocs page for the experiment:

node apps/docs/scripts/scaffold-experiment-doc.mjs my-experiment

Replace TODO sections using src/routes/, types, and wrangler.json. Add "experiments/my-experiment" to apps/docs/content/docs/meta.json under the right category.

See the Experiment Documentation Guide for structure, MDX conventions, and the full checklist.

Submit a pull request

Create a PR with:

  • Clear title: "Add [experiment-name] experiment"
  • Description linking to the experiment idea issue
  • Screenshots or examples of the API in action (if applicable)
  • Confirmation that npx wrangler deploy works

Fill in the PR template with all required information.

Common Patterns and Examples

Using Reference Experiments

You can use existing experiments as references:

Error Handling Patterns

Handling fetch errors:

export async function fetchWithTiming(url: string): Promise<FetchResult> {
  const start = Date.now();
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

  try {
    const res = await fetch(url, {
      method: "GET",
      signal: controller.signal,
      headers: { "User-Agent": "Cloudflare-Experiments/1.0" },
    });
    const responseTimeMs = Date.now() - start;
    clearTimeout(timeoutId);
    return {
      ok: res.ok,
      statusCode: res.status,
      responseTimeMs,
    };
  } catch (e) {
    clearTimeout(timeoutId);
    const responseTimeMs = Date.now() - start;
    const message = e instanceof Error ? e.message : "Unknown error";
    return {
      ok: false,
      statusCode: 0,
      responseTimeMs,
      error: message,
    };
  }
}

Handling JSON parsing errors:

let body: RequestBody;
try {
  body = (await c.req.json()) as RequestBody;
} catch {
  return jsonError(c, "Invalid or missing JSON body", "INVALID_BODY");
}

Handling database constraint errors:

try {
  await db.prepare("INSERT INTO table (id, data) VALUES (?, ?)").bind(id, data).run();
  return jsonSuccess(c, { id });
} catch (e) {
  const err = e as { message?: string };
  if (err.message?.includes("UNIQUE constraint")) {
    return jsonError(c, "Record already exists", "DUPLICATE_ENTRY");
  }
  throw e; // Let global error handler deal with it
}

Deployment Checklist

Before submitting your PR, verify:

  • npm run dev works locally
  • npx wrangler deploy deploys successfully
  • All endpoints return proper JSON responses
  • Error codes are descriptive (e.g. INVALID_URL, not ERROR)
  • Global error handler returns consistent 500 responses
  • README.md documents all API endpoints
  • Bindings are declared in both wrangler.toml and src/types/env.d.ts
  • TypeScript strict mode passes with no errors
  • No any types in the code
  • Added experiment to root README.md table
  • Added or updated docs page at apps/docs/content/docs/experiments/<name>.mdx and meta.json (see Experiment Documentation Guide)

Getting Help

Next Steps

Thank you for contributing to Cloudflare Experiments!

On this page