1---
2title: "Part 2: Add a Worker"
3description: Create a Cloudflare Worker, bind the R2 Bucket, and implement GET/PUT routes.
4sidebar:
5 order: 2
6---
7
8import Terminal from "../../../../components/Terminal.astro";
9import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
10
11In [Part 1](/cloudflare/tutorial/part-1) you deployed an R2 Bucket. Now you'll
12create a Cloudflare Worker that reads and writes objects in that
13bucket over HTTP.
14
15## Create the Worker file
16
17Create `src/worker.ts`. A Worker is a special kind of Resource — it
18has both an infrastructure definition and a runtime implementation
19expressed as an Effect.
20
21```typescript
22// src/worker.ts
23import * as Cloudflare from "alchemy/Cloudflare";
24import * as Effect from "effect/Effect";
25import * as HttpServerResponse from "effect/http/HttpServerResponse";
26
27export default Cloudflare.Worker(
28 "Worker",
29 {
30 main: import.meta.url,
31 },
32 Effect.gen(function* () {
33 return {
34 fetch: Effect.gen(function* () {
35 return HttpServerResponse.text("Hello, world!");
36 }),
37 };
38 }),
39);
40```
41
42:::note
43Notice the "Effect returning an Effect" pattern. The outer generator
44is the **Construction phase** — it runs at both plan time and runtime. The
45inner `fetch` generator is the **Runtime phase** — it runs only when
46handling a request.
47:::
48
49## Bind the Bucket to the Worker
50
51Now let's bind the R2 Bucket from Part 1 to our new Worker. The
52problem is that the Bucket is declared inside the Stack's generator
53in `alchemy.run.ts` — we can't import it from there.
54
55A common pattern in Alchemy is to give each resource its own file.
56Create `src/bucket.ts`:
57
58```typescript
59// src/bucket.ts
60import * as Cloudflare from "alchemy/Cloudflare";
61
62export const Bucket = Cloudflare.R2.Bucket("Bucket");
63```
64
65Update `alchemy.run.ts` to import it instead of declaring it inline:
66
67```diff lang="typescript"
68// alchemy.run.ts
69import * as Alchemy from "alchemy";
70import * as Cloudflare from "alchemy/Cloudflare";
71import * as Effect from "effect/Effect";
72+import { Bucket } from "./src/bucket.ts";
73
74export default Alchemy.Stack(
75 "MyApp",
76 {
77 providers: Cloudflare.providers(),
78 state: Cloudflare.state(),
79 },
80 Effect.gen(function* () {
81- const bucket = yield* Cloudflare.R2.Bucket("Bucket");
82+ const bucket = yield* Bucket;
83
84 return {
85 bucketName: bucket.bucketName,
86 };
87 }),
88);
89```
90
91:::tip
92Resource declarations like `Cloudflare.R2.Bucket("Bucket")` are just
93descriptions — they don't execute until you `yield*` them inside a
94Stack. You can safely import them from multiple files.
95:::
96
97Now the Worker can import `Bucket` and bind it in the Construction phase:
98
99```diff lang="typescript"
100import * as Cloudflare from "alchemy/Cloudflare";
101import * as Effect from "effect/Effect";
102import * as HttpServerResponse from "effect/http/HttpServerResponse";
103+import { Bucket } from "./bucket.ts";
104
105export default Cloudflare.Worker(
106 "Worker",
107 {
108 main: import.meta.url,
109 },
110 Effect.gen(function* () {
111 // @error: ts(2345) Type 'ReadWriteBucket' is not assignable to type 'WorkerServices | PlatformServices'.
112+ const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
113
114 return {
115 fetch: Effect.gen(function* () {
116 return HttpServerResponse.text("Hello, world!");
117 }),
118 };
119 }),
120);
121```
122
123## Provide the binding layer
124
125The previous step showed a type error — `Cloudflare.R2.ReadWriteBucket(Bucket)`
126requires the `Cloudflare.R2.ReadWriteBucketBinding` service. Fix it by piping
127the outer Effect through `Cloudflare.R2.ReadWriteBucketBinding`:
128
129```diff lang="typescript"
130import * as Cloudflare from "alchemy/Cloudflare";
131import * as Effect from "effect/Effect";
132import * as HttpServerResponse from "effect/http/HttpServerResponse";
133import { Bucket } from "./bucket.ts";
134
135export default Cloudflare.Worker(
136 "Worker",
137 { main: import.meta.url },
138 Effect.gen(function* () {
139 const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
140
141 return {
142 fetch: Effect.gen(function* () {
143 return HttpServerResponse.text("Hello, world!");
144 }),
145 };
146+ }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
147);
148```
149
150`ReadWriteBucketBinding` tells the Worker runtime how to look up the
151underlying R2 binding from the Cloudflare environment. Without it, the
152`ReadWriteBucket` binding wouldn't know where to find the bucket at runtime.
153
154## Add the PUT handler
155
156Let's replace the placeholder response with a PUT route that stores
157objects in the bucket. Add `HttpServerRequest` to access the incoming
158request:
159
160```diff lang="typescript"
161import * as Cloudflare from "alchemy/Cloudflare";
162import * as Effect from "effect/Effect";
163+import { HttpServerRequest } from "effect/http/HttpServerRequest";
164import * as HttpServerResponse from "effect/http/HttpServerResponse";
165import { Bucket } from "./bucket.ts";
166
167export default Cloudflare.Worker(
168 "Worker",
169 { main: import.meta.url },
170 Effect.gen(function* () {
171 const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
172 return {
173 fetch: Effect.gen(function* () {
174+ const request = yield* HttpServerRequest;
175+ const key = request.url.split("/").pop()!;
176+
177+ if (request.method === "PUT") {
178+ yield* bucket.put(key, request.stream, {
179+ contentLength: Number(request.headers["content-length"] ?? 0),
180+ });
181+ return HttpServerResponse.empty({ status: 201 });
182+ }
183
184 return HttpServerResponse.text("Hello, world!");
185 }),
186 };
187 }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
188);
189```
190
191TypeScript flags a type error. The `bucket.put` call can fail with
192`R2Error`, but a Worker's `fetch` handler only allows
193`HttpServerError` or `HttpBodyError`. Effect tracks this in the type
194system — you can't forget to handle it.
195
196## Handle R2 errors
197
198Pipe the fetch Effect through `Effect.catchTag` to convert `R2Error`
199into a 500 response:
200
201```diff lang="typescript"
202import * as Cloudflare from "alchemy/Cloudflare";
203import * as Effect from "effect/Effect";
204import { HttpServerRequest } from "effect/http/HttpServerRequest";
205import * as HttpServerResponse from "effect/http/HttpServerResponse";
206import { Bucket } from "./bucket.ts";
207
208export default Cloudflare.Worker(
209 "Worker",
210 { main: import.meta.url },
211 Effect.gen(function* () {
212 const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
213 return {
214 fetch: Effect.gen(function* () {
215 const request = yield* HttpServerRequest;
216 const key = request.url.split("/").pop()!;
217
218 if (request.method === "PUT") {
219 yield* bucket.put(key, request.stream, {
220 contentLength: Number(request.headers["content-length"] ?? 0),
221 });
222 return HttpServerResponse.empty({ status: 201 });
223 }
224
225 return HttpServerResponse.text("Hello, world!");
226+ }).pipe(
227+ Effect.catchTag("R2Error", (error) =>
228+ Effect.succeed(
229+ HttpServerResponse.text(error.message, { status: 500 }),
230+ ),
231+ ),
232+ ),
233 };
234 }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
235);
236```
237
238`Effect.catchTag` matches errors by their `_tag` field. If an R2
239operation fails at runtime, the Worker returns a 500 instead of
240crashing — and the type error disappears because `R2Error` is now
241fully handled.
242
243## Add the GET handler
244
245Complete the fetch handler by reading objects from the bucket when the
246request isn't a PUT:
247
248```diff lang="typescript"
249import * as Cloudflare from "alchemy/Cloudflare";
250import * as Effect from "effect/Effect";
251import { HttpServerRequest } from "effect/http/HttpServerRequest";
252import * as HttpServerResponse from "effect/http/HttpServerResponse";
253import { Bucket } from "./bucket.ts";
254
255export default Cloudflare.Worker(
256 "Worker",
257 { main: import.meta.url },
258 Effect.gen(function* () {
259 const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
260 return {
261 fetch: Effect.gen(function* () {
262 const request = yield* HttpServerRequest;
263 const key = request.url.split("/").pop()!;
264
265 if (request.method === "PUT") {
266 yield* bucket.put(key, request.stream, {
267 contentLength: Number(request.headers["content-length"] ?? 0),
268 });
269 return HttpServerResponse.empty({ status: 201 });
270 }
271
272+ const object = yield* bucket.get(key);
273+ if (object === null) {
274+ return HttpServerResponse.text("Not found", { status: 404 });
275+ }
276+ const text = yield* object.text();
277+ return HttpServerResponse.text(text);
278- return HttpServerResponse.text("Hello, world!");
279 }).pipe(
280 Effect.catchTag("R2Error", (error) =>
281 Effect.succeed(
282 HttpServerResponse.text(error.message, { status: 500 }),
283 ),
284 ),
285 ),
286 };
287 }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
288);
289```
290
291The Worker now handles two routes:
292
293- **`PUT /:key`** — stores the request body in the bucket
294- **`GET /:key`** — retrieves the object, returning 404 if missing
295
296Because `bucket.get` also returns `R2Error`, the `catchTag` you added
297in the previous step already covers it — no additional error handling
298needed.
299
300## Wire the Worker into the Stack
301
302Add the Worker to `alchemy.run.ts` and expose its URL as a stack
303output:
304
305```diff lang="typescript"
306import * as Alchemy from "alchemy";
307import * as Cloudflare from "alchemy/Cloudflare";
308import * as Effect from "effect/Effect";
309import { Bucket } from "./src/bucket.ts";
310+import Worker from "./src/worker.ts";
311
312export default Alchemy.Stack(
313 "MyApp",
314 {
315 providers: Cloudflare.providers(),
316 state: Cloudflare.state(),
317 },
318 Effect.gen(function* () {
319 const bucket = yield* Bucket;
320+ const worker = yield* Worker;
321
322 return {
323 bucketName: bucket.bucketName,
324+ url: worker.url,
325 };
326 }),
327);
328```
329
330## Deploy
331
332Deploy again. Alchemy detects the new Worker and the unchanged Bucket:
333
334<Tabs syncKey="pkgManager">
335 <TabItem label="bun" icon="bun">
336 <Code code="bun alchemy deploy" lang="sh" />
337 </TabItem>
338 <TabItem label="npm" icon="npm">
339 <Code code="npm run alchemy deploy" lang="sh" />
340 </TabItem>
341 <TabItem label="pnpm" icon="pnpm">
342 <Code code="pnpm alchemy deploy" lang="sh" />
343 </TabItem>
344 <TabItem label="yarn" icon="seti:yarn">
345 <Code code="yarn alchemy deploy" lang="sh" />
346 </TabItem>
347</Tabs>
348
349<Terminal content={`[u]Plan[/u]: [g]1 to create[/g]
350
351[g]+[/g] [b]Worker[/b] [d](Cloudflare.Worker)[/d] [c](1 bindings)[/c]
352[s2][g]+[/g] [c]Bucket[/c]
353[d]•[/d] [b]Bucket[/b] [d](Cloudflare.R2.Bucket)[/d]
354
355Proceed?
356◉ Yes ○ No
357[d]•[/d] [b]Bucket[/b] [d](Cloudflare.R2.Bucket)[/d] [d]no change[/d]
358[g]✓[/g] [b]Worker[/b] [d](Cloudflare.Worker)[/d] [g]created[/g]
359[s2][d]• Uploading worker (14.20 KB) ...[/d]
360[s2][d]• Enabling workers.dev subdomain...[/d]
361{
362[s2]bucketName: "myapp-bucket-a1b2c3d4e5",
363[s2]url: "https://myapp-worker-dev-you-abc123.workers.dev",
364}`} />
365
366## Try it out
367
368Use `curl` to write and read an object:
369
370```sh
371# Store an object
372curl -X PUT https://myapp-worker-dev-you-abc123.workers.dev/hello.txt \
373 -d 'Hello, world!'
374
375# Retrieve it
376curl https://myapp-worker-dev-you-abc123.workers.dev/hello.txt
377# → Hello, world!
378```
379
380## Recap
381
382You now have:
383
384- A Cloudflare Worker with GET and PUT routes
385- An R2 Bucket bound to the Worker
386- Stack outputs showing both the bucket name and worker URL
387
388In [Part 3](/cloudflare/tutorial/part-3), you'll learn about stages and state
389stores so multiple developers (and CI) can deploy isolated environments.
390
391:::tip[Want to start building now?]
392You have a deployed Worker — that's enough to skip ahead and
393[add a Durable Object](/cloudflare/compute/durable-objects)
394(or any other Cloudflare deep-dive) if you'd rather build something
395interesting first and circle back to testing, local dev, and CI/CD
396when you need them.
397:::