SearchCtrlK

  • ↑↓navigate
  • ↵open
  • escclose

Can’t find it? Contact us

Make a binding

Declare a KV namespace, D1 database, R2 bucket, queue, rate limit, email sender or Durable Object binding in cloudflare.config.ts with sdxc make binding, then type, use and test it.

After reading this guide, you will know

  • What each make binding helper writes, and which placeholder to replace before deploying
  • How to read the new binding from env once sdxc run typegen types it
  • Where a test gets the binding, since Vitest runs outside the Worker

A binding is how the Worker reaches a Cloudflare resource: a KV namespace, a D1 database, an R2 bucket, a queue. The Worker receives only what cloudflare.config.ts declares under env, and sdxc run typegen types env from the same file. sdxc make binding adds the declaration, in the form the helper takes, so the key you read in code and the resource Cloudflare attaches are named in one place:

Terminal
sdxc make binding kv CACHE
Edited cloudflare.config.ts
Next: sdxc run typegen
--- a/cloudflare.config.ts
+++ b/cloudflare.config.ts
 		env: {
+			CACHE: bindings.kv({ id: "00000000000000000000000000000000" }),

Run the command the last line names, and env.CACHE type checks as a KVNamespace:

Terminal
sdxc run typegen

Helpers

The first argument is the bindings helper, spelled as cf/config spells it. Each writes a declaration with placeholder values, and the last column is what to change before the first deploy.

HelperWritesBefore deploying
kvbindings.kv({ id: "000…" })put the id cf kv namespaces create --title <name> prints
d1bindings.d1({ name: <worker>-<key>, id: "00000000-…" })put the id cf d1 create --name <name> prints
r2bindings.r2({ name: "<key in kebab case>" })create the bucket with cf r2 buckets create --name <name>
queuebindings.queue({ name: <worker>-<key> })create the queue with cf queues create --queue-name <name>
rateLimitbindings.rateLimit({ namespace: "<next free>", simple: { limit: 100, period: 60 } })pick the limit it allows
sendEmailbindings.sendEmail()nothing
durableObjectbindings.durableObject({ worker: manifest.name, exportName: "<Key>DO" })name the Worker that defines the class, when another Worker does, and the class it exports
secretbindings.secret(), and NAME= in .env.exampleset its value with cf workers secrets update, as Make a secret covers

Each named resource gets a name of its own, so a new binding never shares the template's database, queue or counter. d1 and queue name theirs after the Worker and the key, as a template literal over manifest.name. r2 names its bucket after the key in kebab case, as R2 requires, so USER_UPLOADS declares the bucket user-uploads. rateLimit takes the next namespace number the file leaves free.

For a Durable Object class this Worker defines, sdxc make do writes the class, its export and a binding with every name filled in; reach for the durableObject helper for a class another Worker defines.

Names

The key is SCREAMING_SNAKE_CASE. One already in that form is kept as it is, and kebab-case or camelCase is converted, so user-uploads and userUploads both declare USER_UPLOADS. A key that holds anything else is refused:

"api.key" is not a binding name

A new binding is added as the first entry of env, so the newest one is on top. Every file it edits then goes through the project's format command, as with every make kind.

Flags

FlagWhat it does
--dry-runprint the edit as a diff, and change nothing
--forcereplace a binding of that key with the new one

A key env already declares stops the run with the file as it was:

CACHE is already declared in cloudflare.config.ts; pass --force to replace it

Pass --force to swap the binding for a different helper: the new declaration takes the old one's place in env, and the comment above it stays. make binding edits cloudflare.config.ts only, so a Worker configured in wrangler.jsonc stops with No cloudflare.config.ts in the project.

Read it from env

Import env from cloudflare:workers where the binding is used, in the layer that owns what it stores. A KV namespace holding a copy of an outside answer is read through a service:

app/services/exchange-rates.ts
import { env } from "cloudflare:workers";

/** Stores the rates for an hour, so every page in that hour reads one copy. */
export async function saveRates(rates: Record<string, number>): Promise<void> {
	await env.CACHE.put("rates", JSON.stringify(rates), { expirationTtl: 3600 });
}

/** The stored rates, or `null` once they expired. */
export async function readRates(): Promise<Record<string, number> | null> {
	return await env.CACHE.get<Record<string, number>>("rates", "json");
}

Test it

Vitest imports the app's modules in Node, where cloudflare:workers is the stand-in in app/test/cloudflare-workers.ts, and its env holds only the bindings listed there. Add the new one with the mock @sdxc/cloudflare-mocks exports for its helper, which the template already installs: createKVNamespace(), createD1Database(), createR2Bucket(), createQueue(), createRateLimit() or createSendEmail().

app/test/cloudflare-workers.ts
import {
	createD1Database,
	createKVNamespace,
	createQueue,
	createRateLimit,
	createSendEmail,
} from "@sdxc/cloudflare-mocks";

/** The bindings and secrets the app reads, with their development values. */
export const env = {
	DB: createD1Database(),
	EMAIL: createSendEmail(),
	QUEUE: createQueue(),
	RATE_LIMITER: createRateLimit(),
	SESSIONS: createKVNamespace(),
	CACHE: createKVNamespace(),
	SESSION_SECRET: "development-session-secret",
	HONEYPOT_SECRET: "development-honeypot-secret",
	TURNSTILE_SITE_KEY: "1x00000000000000000000AA",
	TURNSTILE_SECRET_KEY: "1x0000000000000000000000000000000AA",
} as Partial<Cloudflare.Env> as Cloudflare.Env;

A binding left out of the stand-in reads as undefined, so a test of code that uses it fails on the first call. With it there, the service is tested through the same env it reads in production:

app/services/exchange-rates.test.ts
import { describe, expect, test } from "vitest";

import { readRates, saveRates } from "~/app/services/exchange-rates";

describe("exchange rates", () => {
	test("reads back the rates it saved", async () => {
		await saveRates({ EUR: 0.92 });

		expect(await readRates()).toEqual({ EUR: 0.92 });
	});
});
Terminal
bun run test app/services/exchange-rates.test.ts

Where to go next

Documentation

Can’t find it? Contact us