# Cloudflare Workers

The Cloudflare adapter exports a Workers module entrypoint: the canonical `export default { fetch }` shape. Service Worker style (`addEventListener("fetch", ...)`) is no longer recommended; the adapter does not emit it.

**Diagram: Workers modules-format request path**

- **Worker fetch(request, env, ctx)** (platform) - export default { fetch }
- **toFetchHandler(app)** (adapter) - or app.decorate(env) by hand
- **app.fetch(Request)** (core) - routing · hooks · handler
- **Response** (platform)

toFetchHandler returns the { fetch } object Workers expect. To reach KV, R2, D1, or secrets, write the module export by hand and app.decorate(env) so bindings land on ctx.state inside every handler.

## When to choose Workers

- You want global, low-latency execution without managing regions yourself.
- You can live without raw TCP sockets (Workers Hyperdrive solves Postgres).
- You want bindings (KV, R2, D1, Durable Objects, Queues) instead of standalone services.

## Scaffold

```bash
pnpm create daloy@latest my-api --template cloudflare-worker
cd my-api
pnpm dev   # wrangler dev under the hood
```

## Worker entrypoint (no bindings)

If you don't need `env` bindings or the Worker `ExecutionContext`, `toFetchHandler` is a one-liner. It returns the `{ fetch }` object Workers expect as the default export, so do **not** wrap it again.

```ts
// src/index.ts
import { toFetchHandler } from "@daloyjs/core/cloudflare";
import { app } from "./server.js";

export default toFetchHandler(app);
```

## wrangler.jsonc

Cloudflare now recommends `wrangler.jsonc` over `wrangler.toml` for new projects; both are still supported. The single `nodejs_compat` flag is all you need on a recent compatibility date, there's no separate `nodejs_compat_v2` to add.

```jsonc
// wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-api",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-22",
  "compatibility_flags": ["nodejs_compat"],

  "kv_namespaces": [
    { "binding": "CACHE", "id": "<kv-id>" }
  ],
  "d1_databases": [
    { "binding": "DB", "database_name": "my-api", "database_id": "<d1-id>" }
  ],
  "placement": { "mode": "smart" }
}
```

## Deploy

```bash
# local dev
pnpm wrangler dev

# secrets (not committed)
pnpm wrangler secret put SESSION_SECRET

# ship it
pnpm wrangler deploy
```

`wrangler publish` was renamed to `wrangler deploy` in 2024. Don't use the old name; some CI templates still reference it.

## Bindings (env)

`toFetchHandler(app)` only forwards the `Request`. To expose Worker bindings (KV, R2, D1, Durable Objects, Queues, Hyperdrive, secrets) to your handlers, write the module-format export by hand and inject the bindings into the app with `app.decorate(...)`. That's how DaloyJS makes runtime values available on `ctx.state` inside every handler.

```ts
// src/index.ts
import { app } from "./server.js";

export interface Env {
  CACHE: KVNamespace;
  DB: D1Database;
  SESSION_SECRET: string;
}

let decorated = false;

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Decorate once per isolate; app.decorate() throws if called twice with
    // the same key, so guard it.
    if (!decorated) {
      app.decorate("env", env);
      app.decorate("waitUntil", (p: Promise<unknown>) => ctx.waitUntil(p));
      decorated = true;
    }
    return app.fetch(request);
  },
};
```

Inside any route handler, read the binding from `ctx.state.env` (the key you passed to `decorate`):

```ts
app.get(
  "/cached/:key",
  {
    request: { params: z.object({ key: z.string() }) },
    responses: { 200: { body: z.object({ value: z.string().nullable() }) } },
  },
  async ({ params, ctx }) => {
    const value = await ctx.state.env.CACHE.get(params.key);
    return { status: 200, body: { value } };
  },
);
```

## Gotchas

- No raw TCP. Use **Hyperdrive** for Postgres/MySQL, or HTTP drivers like Neon's serverless driver, PlanetScale's `@planetscale/database`, or Turso/libSQL. See [Database hosting](/docs/databases).
- No filesystem, so use [multipart uploads](/docs/multipart) with R2, not `node:fs`.
- For background work, decorate the app with a `waitUntil` wrapper (see the bindings example above): `toFetchHandler` alone does not forward the Worker `ExecutionContext`.

## See also

- [Adapters overview](/docs/adapters)
- [Neon on Workers](/docs/databases/neon) · [PlanetScale](/docs/databases/planetscale) · [Turso](/docs/databases/turso) · [Cloudflare D1](/docs/databases/cloudflare-d1)
- [Sessions](/docs/security/session) · [Distributed rate-limit store](/docs/security/rate-limit-redis)

---

Source: https://daloyjs.dev/docs/adapters/cloudflare-workers