Skip to content

Making Requests

There are two ways to send a request. For the common case, the HttpClient service has method accessors — get, post, put, patch, del, head, options — that take a URL and an options object. For anything richer, you build a request value with HttpClientRequest and hand it to client.execute. Both return an Effect that yields a HttpClientResponse.

import { Effect, Schema } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
class CreatedTodo extends Schema.Class<CreatedTodo>("CreatedTodo")({
id: Schema.Number,
title: Schema.String,
completed: Schema.Boolean
}) {}
const program = Effect.gen(function*() {
const client = yield* HttpClient.HttpClient
// 1. The quick path: a GET with query parameters.
const listResponse = yield* client.get("https://api.example.com/todos", {
urlParams: { completed: "false", limit: "20" }
})
// 2. The builder path: construct a request value, then execute it.
// Useful when you want to compose several modifications.
const created = yield* HttpClientRequest.post("https://api.example.com/todos").pipe(
HttpClientRequest.acceptJson,
HttpClientRequest.setHeader("x-request-source", "docs-example"),
// bodyJsonUnsafe serializes a value to a JSON body. It is "unsafe" only in
// that it assumes the value is JSON-encodable; it does not throw at runtime
// for plain data.
HttpClientRequest.bodyJsonUnsafe({ title: "Write docs", completed: false }),
client.execute
)
return { listResponse, created }
})

The method accessors and the HttpClientRequest.* constructors accept the same options (minus method/url, which are already implied). The most common fields:

import { HttpClientRequest } from "effect/unstable/http"
const request = HttpClientRequest.get("https://api.example.com/search", {
// Query string parameters. Accepts a record, an array of pairs, or UrlParams.
urlParams: { q: "effect", page: "1" },
// Request headers. Accepts a record or a Headers value.
headers: { authorization: "Bearer token" },
// URL fragment.
hash: "results",
// Ask the server for a JSON response (sets the Accept header).
acceptJson: true
})

Every HttpClientRequest is an immutable value. The HttpClientRequest module exposes combinators that return a new request, so they compose cleanly under .pipe. The most useful ones:

| Combinator | What it does | | --- | --- | | setUrl / prependUrl / appendUrl | Set or extend the request URL. | | setHeader / setHeaders | Add headers. | | setUrlParam / setUrlParams / appendUrlParam | Set or append query parameters. | | bearerToken / basicAuth | Add an Authorization header. | | accept / acceptJson | Set the Accept header. | | bodyJson / bodyJsonUnsafe | JSON request body. | | bodyText / bodyUint8Array | Raw text or bytes. | | bodyUrlParams | application/x-www-form-urlencoded body. | | bodyFormData / bodyFormDataRecord | multipart/form-data body. | | bodyStream / bodyFile | Streamed body, e.g. for large uploads. | | schemaBodyJson | Encode a value through a schema into a JSON body. |

import { Effect } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
// A request value can be assembled independently of any client and reused.
const search = HttpClientRequest.get("https://api.example.com/search").pipe(
HttpClientRequest.bearerToken("secret-token"),
HttpClientRequest.setUrlParams({ q: "effect", lang: "en" }),
HttpClientRequest.acceptJson
)
const run = Effect.gen(function*() {
const client = yield* HttpClient.HttpClient
return yield* client.execute(search)
})

For JSON, prefer bodyJsonUnsafe for plain data you already trust, and bodyJson when you need the effectful, fully-validated path. To encode a value through a schema (so the request body matches a contract), use schemaBodyJson, which returns an Effect because encoding can fail:

import { Effect, Schema } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
const NewTodo = Schema.Struct({
title: Schema.String,
completed: Schema.Boolean
})
// schemaBodyJson(schema) returns a function from a value to an Effect of the
// request — encoding through the schema may fail, hence the Effect.
const buildRequest = (todo: typeof NewTodo.Type) =>
HttpClientRequest.post("https://api.example.com/todos").pipe(
HttpClientRequest.schemaBodyJson(NewTodo)(todo)
)
const _ = Effect.gen(function*() {
return yield* buildRequest({ title: "Ship it", completed: false })
})

Form and binary bodies work the same way — bodyUrlParams for form-encoded fields, bodyFormDataRecord for multipart uploads, and bodyStream/bodyFile when you want to stream a large payload without buffering it in memory.

Executing a request yields a HttpClientResponse in the success channel and an HttpClientError in the failure channel. By default a non-2xx status is not an error — the response is returned as-is so you can inspect the status. To make a bad status fail, use HttpClient.filterStatusOk (covered under Resilience). Decoding the body is covered next, in Handling responses.