Skip to main content
Contributions should follow the project’s coding and structure rules to keep the repository consistent and maintainable.

Experiment Structure

Every experiment lives under experiments/<name>/ with this standardized layout:
experiments/<name>/
├── src/
│   ├── index.ts          # Hono app, mount routes, export default { fetch: app.fetch }
│   ├── routes/           # Route handlers only (one file per route or logical group)
│   ├── lib/              # Domain logic (fetch, URL validation, parsing)
│   ├── utils/            # Shared helpers (jsonError, jsonSuccess, etc.)
│   ├── constants/        # Optional config/literals
│   └── types/            # All types; env in env.d.ts
├── package.json
├── wrangler.toml         # or wrangler.json; bindings here
├── tsconfig.json
└── README.md

Directory Breakdown

The main entry point for the Worker. This file:
  • Creates a new Hono app instance
  • Mounts route handlers
  • Registers global error handling
  • Exports { fetch: app.fetch }
Example:
import { Hono } from "hono";
import type { Env } from "./types/env";
import checkRoutes from "./routes/check";

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

app.route("/", checkRoutes);

app.get("/", (c) => {
  return c.json({
    name: "is-it-down",
    description: "Check if a website is reachable from Cloudflare's edge",
    usage: "GET /check?url=https://www.cloudflare.com",
  });
});

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

export default {
  fetch: app.fetch,
};
Contains route handlers only. Each file exports a Hono app with one or more related routes.Example from src/routes/check.ts:
import { Hono } from "hono";
import type { Env } from "../types/env";
import { validateUrl } from "../lib/url";
import { fetchWithTiming } from "../lib/fetch";
import { jsonError, jsonSuccess } from "../utils/response";

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

app.get("/check", async (c) => {
  const urlParam = c.req.query("url");
  const url = validateUrl(urlParam);
  if (!url) {
    return jsonError(c, "Missing or invalid query parameter: url", "INVALID_URL");
  }

  const result = await fetchWithTiming(url);
  return jsonSuccess(c, result);
});

export default app;
Domain logic and business functionality. This includes:
  • URL validation
  • Fetch operations
  • Data parsing
  • External API interactions
Example from src/lib/url.ts:
import { ALLOWED_SCHEMES } from "../constants/defaults";

/**
 * 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 (!ALLOWED_SCHEMES.includes(url.protocol as (typeof ALLOWED_SCHEMES)[number])) {
      return null;
    }
    return url.href;
  } catch {
    return null;
  }
}
Shared helper functions used across the experiment.Example from 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 });
}
Optional directory for configuration values and literals.Example from src/constants/defaults.ts:
/** Max time (ms) to wait for the target URL to respond. */
export const FETCH_TIMEOUT_MS = 15_000;

/** Allowed URL schemes for security. */
export const ALLOWED_SCHEMES = ["http:", "https:"] as const;
All TypeScript type definitions. Worker environment bindings go in env.d.ts.Example from src/types/env.d.ts:
/// <reference types="@cloudflare/workers-types" />

export interface Env {
  DB: D1Database;
  LINKS_CACHE: KVNamespace;
}

Core Principles

No shared code between experiments: Each experiment is standalone with its own package.json and dependencies. Do not import from other experiments or the repo root.
Single responsibility: One experiment = one Cloudflare capability (e.g. Workers AI, Browser Rendering, D1).
Edge-first, under ~60 seconds: Prefer stateless, fast request paths that complete quickly.

TypeScript and API Style

Strict TypeScript

  • Use strict TypeScript; avoid any
  • Type Worker env in src/types/env.d.ts and use Hono<{ Bindings: Env }>
  • All types should be explicitly defined

Error Handling

Use a shared helper for client errors:
jsonError(c, message, code, status)
Status code guidelines:
  • 400 for bad request (invalid input)
  • 404 for not found
  • 502 for server/upstream errors (don’t expose internals)
Example:
if (!url) {
  return jsonError(c, "Missing or invalid query parameter: url", "INVALID_URL");
}

Success Responses

Use jsonSuccess(c, data) for JSON success responses:
return jsonSuccess(c, { status: "reachable", responseTime: 87 });

Global Error Handler

In src/index.ts, register app.onError(...) so uncaught errors return consistent JSON:
app.onError((err, c) => {
  return c.json({ error: err.message, code: "INTERNAL_ERROR" }, 500);
});

URL Parameter Validation

For any endpoint that takes a url query param:
  • Validate with a shared validateUrl(input) in src/lib/url.ts
  • Allow only http:// and https://
  • Return jsonError with a clear message and code (e.g. INVALID_URL) when invalid
Example:
const url = validateUrl(urlParam);
if (!url) {
  return jsonError(c, "Missing or invalid query parameter: url", "INVALID_URL");
}

Naming Conventions

  • Error codes: Descriptive uppercase with underscores (e.g. INVALID_URL, FETCH_ERROR, INTERNAL_ERROR)
  • Routes: Files in src/routes/ with kebab-case filenames matching the path (e.g. check.ts for /check)
  • Functions: camelCase for functions and variables
  • Types: PascalCase for interfaces and types

Real-World Examples

Error Handling Pattern

From src/lib/fetch.ts in the is-it-down experiment:
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-IsItDown/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,
    };
  }
}

Database Operations with Error Handling

From src/routes/shorten.ts in the link-shortener experiment:
let code = generateCode();
const maxAttempts = 5;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
  try {
    await db.prepare("INSERT INTO links (code, url) VALUES (?, ?)").bind(code, url).run();
    await setCachedLink(c.env, code, url);
    return c.json({ code, url } satisfies ShortenResponse, 201);
  } catch (e) {
    const err = e as { message?: string };
    if (err.message?.includes("UNIQUE constraint")) {
      code = generateCode();
      continue;
    }
    throw e;
  }
}
return jsonError(c, "Could not generate unique code", "INTERNAL_ERROR", 502);

Configuration Files

wrangler.toml/wrangler.json

Declare all Cloudflare bindings (D1, KV, R2, AI, etc.) in this file. These bindings must also be typed in src/types/env.d.ts.

package.json

Each experiment has its own dependencies. Common dependencies:
  • hono - Web framework
  • @cloudflare/workers-types - TypeScript types for Workers

tsconfig.json

Enable strict mode and configure paths appropriately for the experiment.

Quality Checklist

Before submitting a PR, ensure:
  • TypeScript strict mode is enabled with no any types
  • All errors use jsonError with descriptive codes
  • Global error handler is registered in index.ts
  • URL parameters are validated with validateUrl
  • Environment bindings are declared in both wrangler.toml and src/types/env.d.ts
  • Routes are in src/routes/, logic in src/lib/, helpers in src/utils/
  • The experiment can be deployed with npx wrangler deploy
  • README.md documents the API and usage

Next Steps