Search docs

Jump between documentation pages.

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.

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 — 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_compatflag 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 deployin 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.route({
  method: "GET",
  path: "/cached/:key",
  request: { params: z.object({ key: z.string() }) },
  responses: { 200: { body: z.object({ value: z.string().nullable() }) } },
  handler: async ({ params, ctx }) => {
    const value = await ctx.state.env.CACHE.get(params.key);
    return { status: 200, body: { value } };
  },
});

Gotchas

  • No raw TCP. Use Hyperdrivefor Postgres/MySQL, or HTTP drivers like Neon's serverless driver, PlanetScale's @planetscale/database, or Turso/libSQL. See Database hosting.
  • No filesystem — use multipart uploads 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