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 }
})

A HttpClientRequest is an immutable value: its method, URL, query parameters (urlParams), hash, headers, and body are stored as separate structured fields, and every combinator returns a new request. The base URL, query string, and hash stay separate until the request is converted to a real URL or Web Request, so you can update each part independently.

There are two entry points that produce the same shapes.

The HttpClient accessors (client.get, client.post, …) build the request and execute it in one step — they return an Effect<HttpClientResponse>. There are seven: get, post, put, patch, del (for DELETE), head, and options.

import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"
const run = Effect.gen(function*() {
const client = yield* HttpClient.HttpClient
const res = yield* client.del("https://api.example.com/todos/42")
return res
})

The HttpClientRequest constructors build a request value you can pass around, transform, and later execute with client.execute. Each takes a string | URL plus an optional Options.NoUrl object.

get / post / put / patch / delete / head / options / trace

Section titled “get / post / put / patch / delete / head / options / trace”

One constructor per method. HttpClientRequest.delete is the DELETE constructor; there is also a TRACE constructor.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com/todos")
HttpClientRequest.post("https://api.example.com/todos")
HttpClientRequest.put("https://api.example.com/todos/1")
HttpClientRequest.patch("https://api.example.com/todos/1")
HttpClientRequest.delete("https://api.example.com/todos/1")
HttpClientRequest.head("https://api.example.com/todos/1")
HttpClientRequest.options("https://api.example.com/todos")
HttpClientRequest.trace("https://api.example.com/todos")
// => each yields an immutable HttpClientRequest with the given method

The starting point for all constructors: a GET request with no URL, query parameters, hash, headers, or body.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.empty.method
// => "GET"
HttpClientRequest.empty.url
// => ""

Creates a constructor for an arbitrary method, then call it with a URL. This is what get, post, etc. are built from.

import { HttpClientRequest } from "effect/unstable/http"
const report = HttpClientRequest.make("REPORT" as any)
const req = report("https://api.example.com/calendar")
// => HttpClientRequest with method "REPORT"

The lowest-level constructor: build a request from fully normalized components (method, url, UrlParams, hash Option, Headers, HttpBody). Rarely needed directly — prefer the method constructors plus combinators.

import { Option } from "effect"
import { HttpClientRequest, HttpBody, Headers, UrlParams } from "effect/unstable/http"
const req = HttpClientRequest.makeWith(
"GET",
"https://api.example.com",
UrlParams.empty,
Option.none(),
Headers.empty,
HttpBody.empty
)
// => HttpClientRequest

Type guard for request values.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.isHttpClientRequest(HttpClientRequest.get("/x"))
// => true
HttpClientRequest.isHttpClientRequest({})
// => false

Web Request conversion: fromWeb / toWebResult / toWeb

Section titled “Web Request conversion: fromWeb / toWebResult / toWeb”

Bridge between HttpClientRequest and the platform Request. fromWeb reads a Web Request into a request value; toWebResult returns a Result (failing with UrlParamsError when the URL is invalid); toWeb is the effectful variant.

import { Effect } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
// Web Request -> HttpClientRequest
const fromBrowser = HttpClientRequest.fromWeb(
new Request("https://api.example.com/todos", { method: "POST" })
)
// => HttpClientRequest with method "POST"
// HttpClientRequest -> Result<Request, UrlParamsError>
const result = HttpClientRequest.toWebResult(fromBrowser)
// => Result.Success(Request) | Result.Failure(UrlParamsError)
// HttpClientRequest -> Effect<Request, UrlParamsError>
const effectful = HttpClientRequest.toWeb(fromBrowser, { signal: AbortSignal.timeout(5000) })
// => Effect<Request, UrlParamsError>
void effectful
void result
void Effect

The method accessors and the HttpClientRequest.* constructors accept the same HttpClientRequest.Options (minus method/url, which are already implied — that subset is Options.NoUrl). Every field:

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 (without the leading "#").
hash: "results",
// Set the Accept header to a specific media type.
accept: "application/json",
// Shortcut that sets Accept to application/json (overrides `accept` if both set).
acceptJson: true,
// An HttpBody value (see the body reference below).
body: undefined
})

The full Options interface (the method and url fields are present when constructing via make/modify, omitted in the NoUrl form used by the constructors):

FieldTypeNotes
methodHttpMethodThe HTTP method.
urlstring | URLA URL extracts its search params and hash into structured fields.
urlParamsUrlParams.InputRecord, array of pairs, or UrlParams.
hashstringURL fragment without #.
headersHeaders.InputRecord or Headers.
acceptstringSets the Accept header.
acceptJsonbooleanSets Accept: application/json.
bodyHttpBody.HttpBodyThe request body.

Because a request is immutable, the combinators below each return a new request and compose cleanly under .pipe. A request can be assembled independently of any client and reused.

import { Effect } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
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)
})

Applies an Options object to a request in one call — the same logic the constructors use internally.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.empty.pipe(
HttpClientRequest.modify({ method: "POST", url: "https://api.example.com", acceptJson: true })
)
// => POST request with Accept: application/json

Replaces the HTTP method.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com").pipe(
HttpClientRequest.setMethod("POST")
).method
// => "POST"

Replaces the URL. When given a URL, its search parameters and fragment are extracted into the request’s urlParams and hash fields; a string is kept as-is.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.empty.pipe(
HttpClientRequest.setUrl(new URL("https://api.example.com/x?q=1#top"))
)
// => url "https://api.example.com/x", urlParams { q: "1" }, hash Some("top")

Add a segment to the front or back of the URL, normalizing slashes (inserting or trimming exactly one).

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("/todos").pipe(
HttpClientRequest.prependUrl("https://api.example.com")
).url
// => "https://api.example.com/todos"
HttpClientRequest.get("https://api.example.com/todos").pipe(
HttpClientRequest.appendUrl("42")
).url
// => "https://api.example.com/todos/42"

Transforms the URL string with a function.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com/v1/todos").pipe(
HttpClientRequest.updateUrl((u) => u.replace("/v1/", "/v2/"))
).url
// => "https://api.example.com/v2/todos"

Set or clear the URL fragment (stored without the leading #).

import { Option } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
const withHash = HttpClientRequest.get("https://api.example.com/docs").pipe(
HttpClientRequest.setHash("section-2")
)
withHash.hash
// => Option.some("section-2")
HttpClientRequest.removeHash(withHash).hash
// => Option.none()
void Option

Set one header, or merge a collection of headers (existing values with matching names are replaced).

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com").pipe(
HttpClientRequest.setHeader("x-trace-id", "abc123"),
HttpClientRequest.setHeaders({ "x-region": "us-east", "x-tenant": "acme" })
)
// => request with all three headers set

Set the Accept header to a media type, or directly to application/json.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com").pipe(
HttpClientRequest.accept("text/csv")
)
// => Accept: text/csv
HttpClientRequest.get("https://api.example.com").pipe(
HttpClientRequest.acceptJson
)
// => Accept: application/json

setUrlParam / setUrlParams / appendUrlParam / appendUrlParams

Section titled “setUrlParam / setUrlParams / appendUrlParam / appendUrlParams”

set* replaces existing values for a key; append* keeps existing values so repeated keys are preserved. The single-value variants take key, value; the plural variants take a UrlParams.Input.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com/search").pipe(
HttpClientRequest.setUrlParam("q", "effect"),
HttpClientRequest.setUrlParams({ page: "1", limit: "20" }),
HttpClientRequest.appendUrlParam("tag", "ts"),
HttpClientRequest.appendUrlParams({ tag: "fp" })
)
// => query: q=effect&page=1&limit=20&tag=ts&tag=fp

Set the Authorization header. Both accept plain strings or Redacted values, so secrets do not leak into logs.

import { Redacted } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com").pipe(
HttpClientRequest.bearerToken(Redacted.make("secret-token"))
)
// => Authorization: Bearer secret-token
HttpClientRequest.get("https://api.example.com").pipe(
HttpClientRequest.basicAuth("user", Redacted.make("pass"))
)
// => Authorization: Basic dXNlcjpwYXNz
void Redacted

Body combinators set the request body and, where the body carries it, synchronize the Content-Type and Content-Length headers. Empty and FormData bodies intentionally clear explicit content headers so the runtime can supply multipart boundaries.

Most are pure functions returning a new request. The exceptions are bodyJson, schemaBodyJson, and bodyFile, which return an Effect because encoding (or reading a file) can fail.

The primitive: attach a prebuilt HttpBody value. The other body combinators are thin wrappers over this plus an HttpBody.* constructor.

import { HttpClientRequest, HttpBody } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/upload").pipe(
HttpClientRequest.setBody(HttpBody.text("hello"))
)
// => Content-Type: text/plain, Content-Length: 5

A UTF-8 text body. Content type defaults to text/plain.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/log").pipe(
HttpClientRequest.bodyText("plain log line")
)
// => Content-Type: text/plain
HttpClientRequest.post("https://api.example.com/doc").pipe(
HttpClientRequest.bodyText("<x/>", "application/xml")
)
// => Content-Type: application/xml

A raw byte body. Content type defaults to application/octet-stream.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/blob").pipe(
HttpClientRequest.bodyUint8Array(new Uint8Array([1, 2, 3]), "application/wasm")
)
// => Content-Type: application/wasm, Content-Length: 3

A JSON body via JSON.stringify. “Unsafe” because JSON.stringify can throw (e.g. on a BigInt or a circular reference); fine for plain data you trust.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/todos").pipe(
HttpClientRequest.bodyJsonUnsafe({ title: "Ship it", completed: false })
)
// => Content-Type: application/json

The effectful JSON path: a JSON.stringify failure is captured as HttpBodyError in the error channel instead of throwing.

import { Effect } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
const _ = Effect.gen(function*() {
return yield* HttpClientRequest.post("https://api.example.com/todos").pipe(
HttpClientRequest.bodyJson({ title: "Ship it" })
)
})
// => Effect<HttpClientRequest, HttpBodyError>

Encodes a value through a Schema (so the body matches a contract) and sets it as a JSON body. Returns a function from value to Effect; schema and JSON failures surface as HttpBodyError.

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 value -> Effect of the request.
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 })
})
// => Effect<HttpClientRequest, HttpBodyError>

An application/x-www-form-urlencoded body from URL-param input (record, pairs, or UrlParams).

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/login").pipe(
HttpClientRequest.bodyUrlParams({ username: "ada", password: "secret" })
)
// => Content-Type: application/x-www-form-urlencoded, body "username=ada&password=secret"

A multipart/form-data body from a Web FormData value. The content type and length are left unset so the runtime can generate the multipart boundary.

import { HttpClientRequest } from "effect/unstable/http"
const fd = new FormData()
fd.append("field", "value")
HttpClientRequest.post("https://api.example.com/upload").pipe(
HttpClientRequest.bodyFormData(fd)
)
// => multipart body (boundary added by the runtime)

Builds the FormData for you from a record. Array fields append each item under the same key; primitives are stringified, File/Blob values are appended directly, and null/undefined are skipped.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/upload").pipe(
HttpClientRequest.bodyFormDataRecord({
title: "Report",
tags: ["q1", "draft"],
note: null // skipped
})
)
// => multipart body with title + two tags fields

A streaming byte body for large payloads you do not want to buffer. Optionally provide contentType and contentLength; the type defaults to application/octet-stream.

import { Stream } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/upload").pipe(
HttpClientRequest.bodyStream(
Stream.make(new Uint8Array([1, 2]), new Uint8Array([3, 4])),
{ contentType: "application/octet-stream", contentLength: 4 }
)
)
// => streamed request body
void Stream

Streams a file from disk. Returns an Effect requiring FileSystem; it stats the file to set Content-Length and can fail with PlatformError.

import { Effect } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
const _ = Effect.gen(function*() {
return yield* HttpClientRequest.post("https://api.example.com/upload").pipe(
HttpClientRequest.bodyFile("/tmp/report.pdf", { contentType: "application/pdf" })
)
})
// => Effect<HttpClientRequest, PlatformError, FileSystem>

Assembles the request’s URL, query parameters, and hash into a single URL, returning Option.none() if the result is not a valid URL.

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com/search").pipe(
HttpClientRequest.setUrlParam("q", "effect"),
HttpClientRequest.setHash("top"),
HttpClientRequest.toUrl
)
// => Option.some(URL "https://api.example.com/search?q=effect#top")

The TRACE request constructor (listed above with the other method constructors).

import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.trace("https://api.example.com/debug").method
// => "TRACE"

The HttpMethod module holds the supported method vocabulary used across the HTTP client, server, and routing APIs. Methods are uppercase string literals: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" | "TRACE".

A readonly Set of every supported method literal — handy for membership tests or iteration.

import { HttpMethod } from "effect/unstable/http"
HttpMethod.all.has("PATCH")
// => true

Tuples mapping each method to its short constructor name (note DELETE maps to del).

import { HttpMethod } from "effect/unstable/http"
HttpMethod.allShort
// => [["GET", "get"], ["POST", "post"], ["PUT", "put"], ["DELETE", "del"], ...]

Returns true when a method is treated as able to carry a body, narrowing to HttpMethod.WithBody. The bodyless set is GET, HEAD, OPTIONS, TRACE; DELETE is treated as able to carry a body.

import { HttpMethod } from "effect/unstable/http"
HttpMethod.hasBody("POST")
// => true
HttpMethod.hasBody("GET")
// => false

Runtime refinement before accepting an unknown value as a method. Lowercase names are not valid.

import { HttpMethod } from "effect/unstable/http"
HttpMethod.isHttpMethod("GET")
// => true
HttpMethod.isHttpMethod("get")
// => false

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.