Skip to content

HTTP Client

The HTTP client lets you talk to external APIs as Effects. Instead of a one-shot fetch call, you get a HttpClient service that you can configure once — base URL, default headers, retries, rate limiting, tracing — and then reuse across your application. Requests are values you build up with HttpClientRequest, and responses are decoded with schema-aware helpers from HttpClientResponse, so both the success and failure channels are fully typed.

The modules live under effect/unstable/http:

import {
FetchHttpClient,
HttpClient,
HttpClientRequest,
HttpClientResponse
} from "effect/unstable/http"

Here is the shape of a typical client service. It wraps the HttpClient service with some common middleware, then exposes a few domain methods that decode their responses into schemas:

import { Context, Effect, flow, Layer, Schedule, Schema } from "effect"
import {
FetchHttpClient,
HttpClient,
HttpClientRequest,
HttpClientResponse
} from "effect/unstable/http"
// The domain model we expect the API to return.
class Todo extends Schema.Class<Todo>("Todo")({
userId: Schema.Number,
id: Schema.Number,
title: Schema.String,
completed: Schema.Boolean
}) {}
export class JsonPlaceholder extends Context.Service<JsonPlaceholder, {
getTodo(id: number): Effect.Effect<Todo, JsonPlaceholderError>
}>()("app/JsonPlaceholder") {
static readonly layer = Layer.effect(
JsonPlaceholder,
Effect.gen(function*() {
// Take the base HttpClient and apply middleware that should run on every
// request this service makes.
const client = (yield* HttpClient.HttpClient).pipe(
// Prepend a base URL and ask for JSON on all requests.
HttpClient.mapRequest(flow(
HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com"),
HttpClientRequest.acceptJson
)),
// Turn non-2xx responses into a typed failure.
HttpClient.filterStatusOk,
// Retry transient failures (network errors, 5xx, 429) with backoff.
HttpClient.retryTransient({
schedule: Schedule.exponential(100),
times: 3
})
)
const getTodo = Effect.fn("JsonPlaceholder.getTodo")(function*(id: number) {
return yield* client.get(`/todos/${id}`).pipe(
// Decode the JSON body with the Todo schema.
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)),
// Collapse client and decode errors into one domain error.
Effect.mapError((cause) => new JsonPlaceholderError({ cause }))
)
})
return JsonPlaceholder.of({ getTodo })
})
).pipe(
// Provide the fetch-based implementation of HttpClient.
Layer.provide(FetchHttpClient.layer)
)
}
export class JsonPlaceholderError extends Schema.TaggedErrorClass<JsonPlaceholderError>()(
"JsonPlaceholderError",
{ cause: Schema.Defect }
) {}

A few things worth noticing:

  • HttpClient.HttpClient is a service. You access it from the context and derive a configured client with .pipe(...). The configuration is shared by every request, so you write it once.
  • FetchHttpClient.layer provides the concrete implementation backed by the platform fetch. You provide it at the edge of your application, which makes the client trivial to swap in tests.
  • Errors are values. A request fails with HttpClientError, schema decoding fails with Schema.SchemaError, and mapError lets you fold them into a single domain error you control.

To provide the HttpClient service, see Platform for runtime specifics, and Services & Layers for how layers compose.