Effect HttpClient Usage Guide
Minimal verified recipe (Bun + FetchHttpClient)
Tested against httpbin; includes retries, base URL, schema decoding.
import { FetchHttpClient , HttpClient , HttpClientRequest , HttpClientResponse } from "@effect/platform"
import { Effect , Schema , Schedule } from "effect"
const Args = Schema . Record ( { key : Schema . String , value : Schema . String } )
const GetSchema = Schema . Struct ( { url : Schema . String , args : Args } )
const PostSchema = Schema . Struct ( {
json : Schema . Struct ( { foo : Schema . String } ) ,
headers : Schema . Record ( { key : Schema . String , value : Schema . String } ) ,
} )
const program = Effect . gen ( function * ( ) {
const client = ( yield * HttpClient . HttpClient ) . pipe (
HttpClient . mapRequest ( HttpClientRequest . prependUrl ( "https://httpbin.org" ) ) ,
HttpClient . filterStatusOk ,
HttpClient . retryTransient ( { schedule : Schedule . recurs ( 1 ) } ) ,
)
const getResp = yield * client
. execute (
HttpClientRequest . get ( "/get" ) . pipe (
HttpClientRequest . setUrlParams ( { hello : "world" } ) ,
) ,
)
. pipe ( Effect . andThen ( HttpClientResponse . schemaBodyJson ( GetSchema ) ) )
const postResp = yield * client
. execute (
HttpClientRequest . post ( "/post" ) . pipe (
HttpClientRequest . bodyUnsafeJson ( { foo : "bar" } ) ,
HttpClientRequest . setHeader ( "content-type" , "application/json" ) ,
) ,
)
. pipe ( Effect . andThen ( HttpClientResponse . schemaBodyJson ( PostSchema ) ) )
console . log ( getResp . url , getResp . args )
console . log ( postResp . json , postResp . headers [ "Content-Type" ] )
} ) . pipe ( Effect . provide ( FetchHttpClient . layer ) )
program . pipe ( Effect . runPromise )
Layers : FetchHttpClient.layer (Bun/browser/edge) or NodeHttpClient.layerUndici (Node). Provide once at the edge.
Client shaping : mapRequest(prependUrl), mapRequest(setHeaders defaults), filterStatusOk, retryTransient.
Requests : build via HttpClientRequest.get/post/...; mutate with setHeader, setUrlParams, bodyUnsafeJson, schemaBodyJson.
Responses : prefer HttpClientResponse.schemaBodyJson(schema) / schemaHeaders; use text/stream/matchStatus when needed.
Errors : filterStatusOk fails on non-2xx; wrap in tagged errors for domain context.
Base URL + retries (tim-smart/cheffect CorsProxy) :
HttpClient.mapRequest(HttpClientRequest.prependUrl(...))
HttpClient.filterStatusOk
HttpClient.retryTransient(Schedule.spaced(1000))
Caching + decode (tim-smart/receipts ExchangeRates) :
Client with prependUrl + retry + withTracerPropagation(false)
Decode with HttpClientResponse.schemaBodyJson(Rates); cache with Cache.make.
SDK transform retries (receipts AiWorker/atoms.ts) :
transformClient: HttpClient.retryTransient({ schedule: Schedule.spaced("1 second") })
Provided via FetchHttpClient.layer.
Generated API + cookies (tim-smart/effect-http-play src/client.ts) :
HttpApiClient.make(Api, { baseUrl, transformClient: HttpClient.withCookiesRef(cookies) })
Run with NodeHttpClient.layerUndici.
Schema’d bodies + error translation (livestore scripts/src/shared/netlify.ts) :
HttpClientRequest.schemaBodyJson(...) + httpClient.pipe(HttpClient.filterStatusOk).execute
Wrap errors in Schema.TaggedError.
Generated clients with transforms (livestore sync-s2/http-client-generated.ts) :
setUrlParams, bodyUnsafeJson, matchStatus, optional transformClient to inject auth/headers.
Base client factory :
const makeClient = ( base : string ) =>
HttpClient . HttpClient . pipe (
Effect . andThen ( HttpClient . mapRequest ( HttpClientRequest . prependUrl ( base ) ) ) ,
Effect . andThen ( HttpClient . filterStatusOk ) ,
Effect . andThen ( HttpClient . retryTransient ( { schedule : Schedule . spaced ( "500 millis" ) } ) ) ,
)
Auth decorator :
const authed = ( token : string ) =>
makeClient ( "https://api.example.com" ) . pipe (
HttpClient . mapRequest (
HttpClientRequest . setHeaders ( { authorization : `Bearer ${ token } ` } ) ,
) ,
)
Status branching :
const okSchema = Schema . Struct ( { message : Schema . String } )
const errorSchema = Schema . Struct ( { error : Schema . String } )
const effect = authed ( token )
. execute ( HttpClientRequest . get ( "/hello" ) )
. pipe (
Effect . andThen (
HttpClientResponse . matchStatus ( {
200 : HttpClientResponse . schemaBodyJson ( okSchema ) ,
400 : HttpClientResponse . schemaBodyJson ( errorSchema ) ,
orElse : ( ) => new shouldNeverHappen ( "unexpected status" ) ,
} ) ,
) ,
)
SDK hook : pass transformClient (retries, cookies, tracer propagation toggle) to any client factory that accepts it.
Observability & resilience
Add spans at definition (Effect.withSpan("http-call") or service-level spans).
Use retryTransient for transport errors; Schedule.spaced/exponential with limits.
Decode expected errors into Schema.TaggedError; avoid unknown/any in error channels.
Bun/browser/edge : FetchHttpClient.layer (works in Bun; used in the verified snippet).
Node : NodeHttpClient.layerUndici (use in pure Node runtimes).
Provide exactly one HTTP client layer in the environment.