Every operation returns an Effect, requests go through Effect's HttpClient, and inputs and outputs are Effect Schemas. Distilled's core gives you everything you want from an Effect-first SDK: typed error categories, retry policies, pagination as Effect Streams, and a span per request. Every SDK is built on the same core with the same naming patterns, so once you've used one, the rest feel familiar.
Match an exact error or a whole category with the Effect functions you already know, like Effect.catchTags and Effect.catchIf. Each SDK is patched with the errors and details its API spec never documents, so nothing comes back as unknown and your types stay accurate.
Retries and backoff
const write = Effect.all([
// throttled → waits as long as S3 asks, forever
S3.putObject({ Bucket, Key, Body }),
// no retry of its own; only the outer loop can bring it back
S3.deleteObject({ Bucket, Key }).pipe(AWS.Retry.none),
]).pipe(AWS.Retry.throttling)
// around both: 5xx, timeouts, dropped sockets — 4 tries, then fail
write.pipe(Effect.retry({
while: isTransientError,
times: 3,
schedule: Schedule.exponential("250 millis"),
}))
The API's own Retry-After is respected, and you can define your own retry policies and stack them.
Streaming pagination
// Paginated: pages fetch themselves, on demand.const stale = Lambda.listFunctions.items({}).pipe(
Stream.filter((fn) => fn.Runtime === "nodejs16.x"),
Stream.take(50),
Stream.runCollect,
)
// Bodies stream in and out — nothing is buffered.const copy = S3.getObject({ Bucket: src, Key }).pipe(
Effect.flatMap((o) =>
S3.putObject({ Bucket: dst, Key, Body: o.Body! })),
)
Every paginated operation has .items() and .pages(), so you pull items from an Effect Stream and pages load as you go.Large binary bodies stream in and out without being held in memory. Cancel the Effect and the requests stop.
Layered configuration
const AwsLive = Layer.mergeAll(
FetchHttpClient.layer,
Region.fromEnv(),
Credentials.fromChain(), // env → ~/.aws → SSO → IMDS
)
// the region comes from the layer…const head = S3.headObject({ Bucket, Key })
// ...unless one call says otherwiseconst eu = head.pipe(Effect.provide(Region.of("eu-central-1")))
Effect.all([head, eu]).pipe(Effect.provide(AwsLive))
Your code just calls S3.headObject. Credentials and regions come from layers: provide them once at the root of your program and override them wherever you need to.
Lazy GraphQL
// at runtime `yield* load()` is a single GraphQL queryconst load = Query.fn(() => {
const me = Railway.me()
const page = Railway.projects({ first: 20 })
return {
email: me.email,
names: page.pipe(
Query.map((project) => project.name),
),
}
})
GraphQL queries are abstracted away, just reference the fields you need and a query is generated at runtime.
OpenTelemetry spans
// Effect's HttpClient opens a span per request with the// standard http.* and server.* attributes.
program.pipe(
Effect.withSpan("provision-bucket"),
Effect.provide(OtlpTracer.layer({
url: "http://collector:4318/v1/traces",
})),
)
Operations are ordinary Effects, so they get Effect's built-in OpenTelemetry support.
Strict responses
import { ResponseValidation } from"@distilled.cloud/core"// every SDK checks each response against its schema// (additionalProperties: allow fields the spec does not list)
program.pipe(Effect.provide(ResponseValidation.strict))
// a response that drifted from the spec is a typed error
Neon.getProject({ project_id }).pipe(
Effect.catchTag("NeonParseError", (e) => ...),
)
Responses are lenient by default and come back as the API sent them. Provide one layer and every SDK in your program checks each response against its schema: strict also rejects fields the spec does not list, additionalProperties lets them through. When an API changes shape, the call fails with a typed ParseError instead of handing you the wrong type.
Per-operation imports
// the bundle keeps getObject — not the other 111 S3 opsimport { getObject } from"@distilled.cloud/aws/s3"
gzipped, one S3 operation
118.4 KB
gzipped, one Workers operation
61.7 KB
Every operation is its own export, so the bundler keeps only the ones you call — 1 of 112 S3 operations survives in the measured build. Runs on Node, Bun and Workers. See all the numbers →
The problem
The spec should be the only source. It never is.
Every generated SDK: ours, the vendor's, the one you'd write; is only ever as good as the api spec its built from. Most of these specs aren't generated from the source and regularly drift from the actual implementation (or in cloudflare's case are just blatantly wrong). The spec says a response value is required, your types compile, your runtime fails because the server doesn't return it. We patch the api spec to ensure all inputs, outputs, and errors are correct with live behavior of the providers.
What Fly's spec declaresPOST /v1/apps/{app_name}/machines
responses:"200": OK
— that's it —
One response. No 400 for a bad config, no 403 for a scoped token, no 409 when the name is taken.
What Distilled shipsFly.machines.createMachine
export typeCreateMachineError =
| BadRequest// 400 · seen live | Forbidden// 403 · seen live | NotFound// 404 | Conflict// 409 · name-taken race
| FlyIoOpError
Four typed failures in the operation's error union, each added as a patch to the spec the moment a real deploy surfaced it.
33,995spec fixes, and counting
26 of 87providers have needed at least one — including all 9 that Alchemy runs on
1 in 4operations needed a fix on the median provider Alchemy runs on The full tally →
How it works
Generated from the spec. Patched by Alchemy.
How do we patch specs for so many provider and keep stuff accurate? We have a feedback loop with Alchemy; Alchemy uses Distilled to generate resources and run its test suite against the real cloud provider apis, when the tests fail they are investigated and the patches are added back to Distilled for everyone to benefit!
Alchemy
Infrastructure as code.
Resources: Bucket, Table, Machine
Tests against the actual API
Distilled
Effect-native SDKs.
Schemas, operations, typed errors
Retry, pagination, streaming
Spec
The provider's own API spec.
Providers
87 providers, one shape.
Each provider sdk is one package, produced from the spec and refined by our feedback look and published to npm as @distilled.cloud/<provider>. Pair it with effect. Some specs need a lot of patches see the Wall of Shame →