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-experimentUse kebab-case for the experiment name (e.g. is-it-down, link-shortener).
Set up the project structure
Create the standard directory structure:
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 installCreate 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 devTest your endpoints:
curl http://localhost:8787/my-routeVerify:
- 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=valueResponse
{
"result": "data"
}Errors
400- Invalid parameter
Run locally
cd apps/experiments/my-experiment
npm install
npm run devDeploy
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-experimentReplace 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 deployworks
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 devworks locally -
npx wrangler deploydeploys successfully - All endpoints return proper JSON responses
- Error codes are descriptive (e.g.
INVALID_URL, notERROR) - Global error handler returns consistent 500 responses
- README.md documents all API endpoints
- Bindings are declared in both
wrangler.tomlandsrc/types/env.d.ts - TypeScript strict mode passes with no errors
- No
anytypes in the code - Added experiment to root README.md table
- Added or updated docs page at
apps/docs/content/docs/experiments/<name>.mdxandmeta.json(see Experiment Documentation Guide)
Getting Help
Next Steps
- Review the Code Standards for implementation details
- Check the Contributing Guide for PR guidelines
- Browse existing experiments for inspiration
- Open an experiment idea issue to get started
Thank you for contributing to Cloudflare Experiments!