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.
Last updated
After reading this guide, you will know
- What each
make bindinghelper writes, and which placeholder to replace before deploying - How to read the new binding from
envoncesdxc run typegentypes 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:
sdxc make binding kv CACHEEdited 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:
sdxc run typegenHelpers
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.
| Helper | Writes | Before deploying |
|---|---|---|
kv | bindings.kv({ id: "000…" }) | put the id cf kv namespaces create --title <name> prints |
d1 | bindings.d1({ name: <worker>-<key>, id: "00000000-…" }) | put the id cf d1 create --name <name> prints |
r2 | bindings.r2({ name: "<key in kebab case>" }) | create the bucket with cf r2 buckets create --name <name> |
queue | bindings.queue({ name: <worker>-<key> }) | create the queue with cf queues create --queue-name <name> |
rateLimit | bindings.rateLimit({ namespace: "<next free>", simple: { limit: 100, period: 60 } }) | pick the limit it allows |
sendEmail | bindings.sendEmail() | nothing |
durableObject | bindings.durableObject({ worker: manifest.name, exportName: "<Key>DO" }) | name the Worker that defines the class, when another Worker does, and the class it exports |
secret | bindings.secret(), and NAME= in .env.example | set 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 nameA 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
| Flag | What it does |
|---|---|
--dry-run | print the edit as a diff, and change nothing |
--force | replace 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 itPass --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:
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().
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:
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 });
});
});bun run test app/services/exchange-rates.test.tsWhere to go next
Make a secret — an API key or signing key, declared with its development value.
Make a Durable Object — a class of this Worker with its binding filled in.
Deploy your app — create the resources the bindings name, and write their ids back.