Functions & Servers
Cloudflare Workers, AWS Lambda Functions, ECS Tasks, and Containers are Resources that ship runtime code along with their infrastructure. When you declare one you describe both:
- The cloud configuration (memory, region, compatibility flags…)
- The Effect that runs inside it
Both deploy together as one resource.
The Effectful Constructor pattern
Section titled “The Effectful Constructor pattern”Every Function and Server is declared with the same shape — an Effect that declares its Bindings, then returns its interface:
Effect.gen(function* () { // 1. Declare Bindings — the resources this program uses. const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
// 2. Return the interface — the Effects and functions it exposes. return { fetch: Effect.gen(function* () { /* use bucket */ }), };}); // + Effect.provide(<the binding Layer>) — see BindingsThis is the Effectful Constructor — one simple, repeatable pattern you’ll use everywhere across alchemy:
- A Worker or Lambda Function returns its handlers (
fetch). - A Durable Object returns its RPC methods.
- A Workflow returns its run function.
- A Layer returns a service interface.
Bind what you need, return what you expose. Learn it once and every runtime in alchemy is a variation on it.
fetch is the interface member that serves HTTP — a per-request
Effect. Yield HttpServerRequest to read the request; return an
HttpServerResponse:
import * as Effect from "effect/Effect";import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
Effect.gen(function* () { return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; return HttpServerResponse.text(`hello from ${request.url}`); }), };});Every Worker and Lambda Function serves HTTP through this member — it runs per request, inside the deployed Function (Phases covers exactly when).
The interface isn’t limited to fetch — add methods returning
Effect or Stream, and any resource that binds this Worker calls
them through a typed client:
export default class Greeter extends Cloudflare.Worker<Greeter>()( "Greeter", { main: import.meta.url }, Effect.gen(function* () { return { greet: (name: string) => Effect.succeed(`hello ${name}`), fetch: Effect.gen(function* () { return HttpServerResponse.text("ok"); }), }; }),) {}No schema, no runtime validation — serialization is automatic and the client type is inferred from the class. This is Schemaless RPC, the default for internal calls; the APIs section covers it plus the schema’d surfaces (Effect RPC, Effect HTTP) for trust boundaries.
A Worker, end to end
Section titled “A Worker, end to end”Cloud configuration, a Binding, and a fetch handler together:
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import { Bucket } from "./bucket.ts";
export default Cloudflare.Worker( "Api", { main: import.meta.url, compatibility: { date: "2026-01-28" }, observability: { enabled: true }, }, Effect.gen(function* () { // Init: bind a resource. The binding is the typed SDK. const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
return { // Runtime: per-request handler. fetch: Effect.gen(function* () { const obj = yield* bucket.get("hello.txt"); return obj ? HttpServerResponse.text(yield* obj.text()) : HttpServerResponse.text("Not found", { status: 404 }); }), }; }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),);The props (compatibility, observability, memory, routes…)
are the cloud configuration; the Effect is the Effectful Constructor.
One deploy ships both.
Three ways to declare one
Section titled “Three ways to declare one”Inline — no class, the form used above. Fine when nothing else needs to reference the Worker by name:
export default Cloudflare.Worker("Api", { main: import.meta.url }, effect);Inline class — the same declaration, but the class name makes
the type nominal instead of structural: hovers, errors, and Stack
outputs say Api instead of an anonymous shape:
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url }, effect,) {}Tagged — split identity from implementation. The class declares
only the Tag; .make(props, effect) produces the implementation
Layer:
// src/Api.ts — the Tag: a lightweight identifier others bind toexport class Api extends Cloudflare.Worker<Api, {}>()("Api") {}
// same file, or a separate one — the implementation Layerexport default Api.make( { main: import.meta.url }, Effect.gen(function* () { /* the Effectful Constructor */ }),);The implementation Layer is provided to the Stack, not to another Worker:
import ApiLive, { Api } from "./src/Api.ts";
export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers(), state: Cloudflare.state() }, Effect.gen(function* () { const api = yield* Api; return { url: api.url }; }).pipe(Effect.provide(ApiLive)),);The split matters because other Functions bind to the Tag —
never the implementation. Importing a Tag pulls in no runtime code,
so a Worker binding to Api doesn’t bundle Api’s implementation —
a piece of code with its own dependencies that may not even target
the same runtime (workerd vs node vs bun, serverless vs serverful).
It also allows multiple implementations of one Tag: ship the Tag,
and let each consumer provide the Layer that fits. This split is
what makes
Circular Bindings
possible.
Bindings in action
Section titled “Bindings in action”The yield* ReadWriteBucket(Bucket) line above is a binding —
it connects a resource to the Function’s runtime:
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);const kv = yield* Cloudflare.KV.ReadWriteNamespace(Sessions);
// Inside fetch:yield* bucket.put("key", "value");yield* kv.get("session-id");Every binding obeys the same shape across providers — a Cloudflare
R2 binding and an AWS S3 binding are interchangeable from the
caller’s point of view. Every binding is a contract (the yield*)
plus an interchangeable implementation Layer (the Effect.provide) —
Bindings
covers the deploy-time mechanics.
Effect handlers vs async handlers
Section titled “Effect handlers vs async handlers”Workers and Lambda Functions support two styles for the runtime code. Both deploy through the same provider and produce the same artifact.
Effect style — handlers are Effects, with typed errors, composable retries, structured concurrency, and bindings resolved through Effect’s context:
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket); return { fetch: Effect.gen(function* () { /* ... */ }), }; }),);Async style — handlers are standard async fetch functions.
Bindings are declared on the resource’s env prop and typed via
InferEnv:
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { Bucket },});import type { WorkerEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: WorkerEnv) { const object = await env.Bucket.get("key"); return new Response(object?.body ?? null); },};Pick whichever fits your team. The Effect style unlocks Layers, structured retries, and fine-grained testing; the async style integrates better with existing handler code.
Where next
Section titled “Where next”- Bindings — the deploy-time
mechanics behind
yield* ReadWriteBucket(Bucket): IAM, env injection, typed SDKs. - RPC — how bound resources call the methods you return.
- Phases — when the constructor runs vs the interface it returns, and why.
- Custom Runtime — bring the Functions & Servers model to a new compute target.