Skip to content

jbt95/effect-cf

Repository files navigation

effect-cf

Typed Effect clients for Cloudflare Workers with schema validation.

Modules

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"

Installation

npm install effect-cf
# or
pnpm add effect-cf
# or
yarn add effect-cf

Design Philosophy

  • 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

Quick Example

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);

Complete Application Example

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));

Durable Objects Server Example

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));
    });
  }
}

Development

pnpm install
pnpm build
pnpm test
pnpm typecheck

API Pattern

All 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

Service-Specific Patterns

Storage Services (KV, R2, D1, Cache)

  • get(key)Effect<A \| null, Error>
  • getOrFail(key)Effect<A, Error> (throws NotFoundError)
  • put(key, value)Effect<void, Error>
  • delete(key)Effect<void, Error>

Durable Objects

  • EffectDurableObject base class for implementing DOs
  • fetch(request)Effect<Response, Error>
  • alarm()Effect<void, Error> (optional)
  • storage → Effect-wrapped storage API

Workers Runtime

  • serve(handler) → Creates ExportedHandler
  • waitUntil(effect) → Background task execution
  • passThroughOnException() → Error passthrough

License

MIT

About

Typed Effect clients for Cloudflare Workers with schema validation.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors