Typed Effect clients for Cloudflare Workers with schema validation.
| Module | Description | Import |
|---|---|---|
| core | Shared errors and schema helpers | import * as Core from "effect-cf/core" |
| kv | KV client with type-safe operations | import * as KV from "effect-cf/kv" |
| cache | Cache client with JSON support | import * as Cache from "effect-cf/cache" |
| durable-objects | Full Durable Objects (server + client) | import * as DurableObjects from "effect-cf/durable-objects" |
| r2 | R2 object storage (S3-compatible) | import * as R2 from "effect-cf/r2" |
| d1 | D1 SQLite database | import * as D1 from "effect-cf/d1" |
| workers | Workers runtime utilities | import * as Workers from "effect-cf/workers" |
| queues | Message queues for async processing | import * as Queue from "effect-cf/queues" |
| ai-gateway | AI model inference gateway | import * as AIGateway from "effect-cf/ai-gateway" |
| vectorize | Vector database for AI/ML | import * as Vectorize from "effect-cf/vectorize" |
| hyperdrive | Database connection pooling | import * as Hyperdrive from "effect-cf/hyperdrive" |
| testing | Test utilities with Miniflare | import * as Testing from "effect-cf/testing" |
npm install effect-cf
# or
pnpm add effect-cf
# or
yarn add effect-cf- Namespaced APIs: Every service is accessible through its own namespace (
KV,Cache,DurableObjects,R2,D1) - Type Safety: Full schema validation via
@effect/schema - Ergonomic: Factory functions for direct use, Layers for Effect dependency injection
- Explicit: No magic, everything is typed and predictable
- Complete: Both client-side (calling services) and server-side (implementing services) support
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "@effect/schema/Schema";
import * as KV from "effect-cf/kv";
const User = Schema.Struct({
id: Schema.String,
name: Schema.String
});
// Method 1: Direct factory usage
const kv = KV.make({
type: "json",
namespace: env.KV_USERS,
schema: User
});
const program = Effect.gen(function* () {
yield* kv.put("user:1", { id: "1", name: "Ada" });
const user = yield* kv.getOrFail("user:1");
return user;
});
// Method 2: Service accessors with Layer
const program2 = Effect.gen(function* () {
yield* KV.put("user:1", { id: "1", name: "Ada" });
const user = yield* KV.getOrFail("user:1");
return user;
});
const runnable = program2.pipe(
Effect.provide(KV.layer({ type: "json", namespace: env.KV_USERS, schema: User }))
);
Effect.runPromise(runnable);import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "@effect/schema/Schema";
import * as Workers from "effect-cf/workers";
import * as KV from "effect-cf/kv";
import * as D1 from "effect-cf/d1";
import * as R2 from "effect-cf/r2";
const User = Schema.Struct({
id: Schema.String,
name: Schema.String,
avatar: Schema.String
});
// Worker handler using multiple services
const handler = (request: Request, env: Env) =>
Effect.gen(function* () {
const url = new URL(request.url);
if (url.pathname === "/api/users") {
// Store metadata in D1
const user = yield* D1.queryFirst<User>(
"SELECT * FROM users WHERE id = ?",
url.searchParams.get("id")
);
if (!user) {
return new Response("Not found", { status: 404 });
}
// Fetch avatar from R2
const avatar = yield* R2.getOrFail(user.avatar);
// Cache the response
yield* Workers.waitUntil(
Effect.gen(function* () {
yield* KV.put(`cache:${user.id}`, user);
})
);
return Response.json(user);
}
return new Response("Not found", { status: 404 });
});
// Export worker
export default Workers.serve(handler);
// Or with Layer composition
const AppLayer = Layer.mergeAll(
KV.layer({ type: "json", namespace: env.KV, schema: User }),
D1.layer(env.DB),
R2.layer(env.R2_BUCKET)
);
export default Workers.serve((req, env) => Effect.provide(handler(req, env), AppLayer));import * as DurableObjects from "effect-cf/durable-objects";
import * as Effect from "effect/Effect";
// Implement a counter DO
export class Counter extends DurableObjects.EffectDurableObject {
fetch(request: Request) {
return Effect.gen(function* () {
const url = new URL(request.url);
if (url.pathname === "/increment") {
const current = (yield* this.storage.get<number>("count")) ?? 0;
yield* this.storage.put("count", current + 1);
return new Response(String(current + 1));
}
const count = (yield* this.storage.get<number>("count")) ?? 0;
return new Response(String(count));
});
}
}pnpm install
pnpm build
pnpm test
pnpm typecheckAll modules follow a consistent pattern:
| Export | Purpose | Example |
|---|---|---|
make() |
Factory function | KV.make({...}) |
layer() |
Effect Layer | KV.layer({...}) |
Tag |
Service identifier | KV.KvTag |
get, put, etc. |
Service accessors | KV.get, KV.put |
Types |
Type namespace | KV.Types.KvNamespaceLike |
get(key)→Effect<A \| null, Error>getOrFail(key)→Effect<A, Error>(throws NotFoundError)put(key, value)→Effect<void, Error>delete(key)→Effect<void, Error>
EffectDurableObjectbase class for implementing DOsfetch(request)→Effect<Response, Error>alarm()→Effect<void, Error>(optional)storage→ Effect-wrapped storage API
serve(handler)→ Creates ExportedHandlerwaitUntil(effect)→ Background task executionpassThroughOnException()→ Error passthrough
MIT