A fully typed Cloudflare SDK for Effect, generated from the Cloudflare OpenAPI specification.
- Generated from OpenAPI spec — 1:1 compatibility with Cloudflare APIs
- Typed errors — All errors are
TaggedErrorclasses for pattern matching - Effect-native — All operations return
Effect<A, E, R>with typed errors - Automatic pagination — Stream pages or items with
.pages()and.items()
npm install distilled-cloudflare effect @effect/platformimport { Effect, Layer } from "effect";
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
import * as R2 from "distilled-cloudflare/r2";
import { Auth } from "distilled-cloudflare";
const program = R2.listBuckets({ account_id: "your-account-id" });
const CloudflareLive = Layer.mergeAll(FetchHttpClient.layer, Auth.fromEnv());
program.pipe(Effect.provide(CloudflareLive), Effect.runPromise);import * as R2 from "distilled-cloudflare/r2";
import * as KV from "distilled-cloudflare/kv";
import * as Workers from "distilled-cloudflare/workers";
import * as Queues from "distilled-cloudflare/queues";
import * as Workflows from "distilled-cloudflare/workflows";
import * as DNS from "distilled-cloudflare/dns";import { Auth } from "distilled-cloudflare";
// From environment (CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL)
Effect.provide(Auth.fromEnv())
// Static token
Effect.provide(Auth.fromToken("your-api-token"))
// OAuth
Effect.provide(Auth.fromOAuth({
clientId: "...",
clientSecret: "...",
refreshToken: "...",
}))import { Effect } from "effect";
import * as R2 from "distilled-cloudflare/r2";
R2.getBucket({ account_id: "...", bucket_name: "missing" }).pipe(
Effect.catchTags({
NoSuchBucket: (e) => Effect.succeed({ found: false }),
CloudflareNetworkError: (e) => Effect.fail(new Error("Network error")),
}),
);// Stream all pages
const pages = yield* R2.listBuckets.pages({ account_id: "..." });
// Stream all items
const keys = yield* KV.listKeys.items({ account_id: "...", namespace_id: "..." });| Variable | Required | Description |
|---|---|---|
CLOUDFLARE_API_TOKEN |
Yes | API token for authentication |
CLOUDFLARE_ACCOUNT_ID |
Yes | Account ID for account-scoped operations |
CLOUDFLARE_ZONE_ID |
No | Zone ID for zone-scoped operations (e.g., DNS, ACM read) |
CLOUDFLARE_ACM_ZONE_ID |
No | Zone ID for a zone with Advanced Certificate Manager enabled |
bun vitest run # Run all tests
bun vitest run ./test/services/r2.test.ts # Run tests for a single service
DEBUG=1 bun vitest run ... # Run with request/response debug logsSome tests require a CLOUDFLARE_ZONE_ID to run. If it is not set, those tests are skipped.
The ACM (Advanced Certificate Manager) tests use two separate zone IDs:
CLOUDFLARE_ZONE_ID— Used for read operations (getTotalTl) and error tests that expectAdvancedCertificateManagerRequired(i.e., a zone without ACM).CLOUDFLARE_ACM_ZONE_ID— Used for write operations (createTotalTl) happy paths that require ACM to be enabled on the zone.
If CLOUDFLARE_ACM_ZONE_ID is not set, the three createTotalTl happy path tests are skipped:
happy path - enables Total TLS for a zonehappy path - disables Total TLS for a zonehappy path - enables Total TLS with certificate authority
bun generate # Generate from cached spec
bun generate --fetch # Fetch latest spec and generate
bun test # Run tests
bun tsc -b # Type checkMIT