Skip to content

Request & Response

Every HTTP handler in Effect does two things: it reads from the incoming request and returns a response. Both are plain immutable values backed by a small set of shared data types — Headers, UrlParams, Url, HttpBody, and Cookies — that the same modules use on the client side too.

The request is read from context through the HttpServerRequest service. The response is built with the HttpServerResponse constructors and combinators, all of which are immutable: every combinator returns a new response.

import { Effect, Schema } from "effect"
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
const CreateUser = Schema.Struct({
name: Schema.String,
email: Schema.String
})
// A handler: read the JSON body with a schema, return a JSON response
const handler = Effect.gen(function* () {
// Read request metadata directly from the service
const request = yield* HttpServerRequest.HttpServerRequest
console.log(request.method) // => "POST"
console.log(request.url) // => "/users"
// Decode the body against a schema (fails with SchemaError on bad input)
const body = yield* HttpServerRequest.schemaBodyJson(CreateUser)
// Build a 201 JSON response
return yield* HttpServerResponse.json(
{ id: 1, ...body },
{ status: 201 }
)
})

HttpServerRequest.HttpServerRequest is a Context.Service. Yielding it gives you the request value, whose metadata is available synchronously while body access is effectful (reading and parsing can fail).

import { Effect, Option } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const inspect = Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest
req.method // => "GET" | "POST" | ... (HttpMethod)
req.url // => "/search?q=effect" (path without scheme/host)
req.originalUrl // => the full original URL string
req.headers // => Headers (lowercase-keyed record)
req.cookies // => { session: "abc123" } (parsed from the Cookie header)
req.remoteAddress // => Option<string>
// Body accessors are Effects (inherited from HttpIncomingMessage)
const text = yield* req.text // => string
const json = yield* req.json // => parsed JSON value
const buffer = yield* req.arrayBuffer // => ArrayBuffer
req.stream // => Stream<Uint8Array, HttpServerError>
// Derive a modified view without mutating the original
const rewritten = req.modify({
url: "/v2" + req.url,
remoteAddress: Option.some("127.0.0.1")
})
return rewritten
})

Rather than parse raw values by hand, decode directly into a typed value. Each helper is an Effect that fails with Schema.SchemaError (and HttpServerError when reading the body itself can fail).

import { Effect, Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const Login = Schema.Struct({
username: Schema.String,
password: Schema.String
})
// JSON body → typed value
const fromJson = HttpServerRequest.schemaBodyJson(Login)
// application/x-www-form-urlencoded body → typed value
const fromForm = HttpServerRequest.schemaBodyUrlParams(Login)
// Either multipart OR urlencoded form, chosen by content-type
const fromAnyForm = HttpServerRequest.schemaBodyForm(Login)

The service tag and the request model. Yield the tag to get the current request; its fields are described above.

import { Effect } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const method = Effect.map(
HttpServerRequest.HttpServerRequest,
(req) => req.method
)
// => Effect<HttpMethod, never, HttpServerRequest>

Reads the request body as JSON and decodes it with the supplied schema.

import { Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const User = Schema.Struct({ id: Schema.Number, name: Schema.String })
const decoded = HttpServerRequest.schemaBodyJson(User)
// => Effect<{ id: number; name: string }, HttpServerError | SchemaError, HttpServerRequest>

Decodes the body as form data: multipart requests are persisted and decoded as multipart, other requests are decoded from URL-encoded parameters. Requires Scope, FileSystem, and Path for the multipart path.

import { Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const Form = Schema.Struct({ title: Schema.String })
const decoded = HttpServerRequest.schemaBodyForm(Form)
// => Effect<{ title: string }, MultipartError | SchemaError | HttpServerError, ...>

Builds a decoder that reads a JSON value out of a single named form field (works for both multipart fields and URL-encoded parameters).

import { Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const Payload = Schema.Struct({ tags: Schema.Array(Schema.String) })
const decodeField = HttpServerRequest.schemaBodyFormJson(Payload)
const decoded = decodeField("metadata") // reads the "metadata" field as JSON

Reads the body as application/x-www-form-urlencoded parameters and decodes them with the schema.

import { Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const Search = Schema.Struct({ q: Schema.String })
const decoded = HttpServerRequest.schemaBodyUrlParams(Search)
// => Effect<{ q: string }, HttpServerError | SchemaError, HttpServerRequest>

Persists the multipart body and decodes it with the schema. Requires Scope, FileSystem, and Path to persist uploaded files.

import { Schema } from "effect"
import { HttpServerRequest, Multipart } from "effect/unstable/http"
const Upload = Schema.Struct({
title: Schema.String,
file: Multipart.SingleFileSchema
})
const decoded = HttpServerRequest.schemaBodyMultipart(Upload)

Decodes a schema from the request’s parsed cookies.

import { Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const Session = Schema.Struct({ session: Schema.String })
const cookies = HttpServerRequest.schemaCookies(Session)
// => Effect<{ session: string }, SchemaError, HttpServerRequest>

Decodes a schema from the request headers (header names are lowercase).

import { Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const Auth = Schema.Struct({ authorization: Schema.String })
const headers = HttpServerRequest.schemaHeaders(Auth)
// => Effect<{ authorization: string }, SchemaError, HttpServerRequest>

Decodes a schema from the parsed query string. Requires the ParsedSearchParams service (provided by the router / adapter).

import { Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const Query = Schema.Struct({ q: Schema.String, page: Schema.optional(Schema.String) })
const params = HttpServerRequest.schemaSearchParams(Query)
// => Effect<{ q: string; page?: string }, SchemaError, ParsedSearchParams>

Context.Service holding the decoded query parameters for the current request. Each key maps to a string, or an array when the parameter repeats.

import { Effect } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const all = HttpServerRequest.ParsedSearchParams
// => Effect<ReadonlyRecord<string, string | Array<string>>, never, ParsedSearchParams>

Converts a URL’s search parameters into a record; repeated keys become arrays in insertion order.

import { HttpServerRequest } from "effect/unstable/http"
HttpServerRequest.searchParamsFromURL(new URL("https://x.dev/?a=1&a=2&b=3"))
// => { a: ["1", "2"], b: "3" }

Creates a Channel backed by the current request’s upgraded socket — the building block for WebSocket / raw socket handlers.

import { HttpServerRequest } from "effect/unstable/http"
const channel = HttpServerRequest.upgradeChannel()
// => Channel reading socket frames and writing string | Uint8Array | CloseEvent

The lower-level request.upgrade accessor yields the Socket directly:

import { Effect } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const socket = Effect.flatMap(
HttpServerRequest.HttpServerRequest,
(req) => req.upgrade // => Effect<Socket, HttpServerError>
)

Wrap a Web Request (or an HttpClientRequest) as an HttpServerRequest — handy for adapters and tests.

import { HttpServerRequest } from "effect/unstable/http"
const req = HttpServerRequest.fromWeb(
new Request("https://example.com/users", { method: "POST" })
)
req.method // => "POST"
req.url // => "/users"

Convert an HttpServerRequest to a Web Request. toWebResult returns a Result synchronously; toWeb returns an Effect that streams the body using the current context.

import { Effect } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
const program = Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest
const web = yield* HttpServerRequest.toWeb(req)
return web // => globalThis.Request
})

Convert a server request into an HttpClientRequest, preserving method, headers, and body — useful for proxying.

import { HttpServerRequest } from "effect/unstable/http"
declare const req: HttpServerRequest.HttpServerRequest
const clientReq = HttpServerRequest.toClientRequest(req)

Builds an absolute URL for the request (host from the host header, defaulting to localhost; https only when x-forwarded-proto is https). Returns Option.none() for invalid URLs.

import { HttpServerRequest } from "effect/unstable/http"
declare const req: HttpServerRequest.HttpServerRequest
HttpServerRequest.toURL(req) // => Option<URL>

Re-exported fiber reference controlling the maximum body size accepted while reading request bodies. Set it with Effect.provideService (or the fiber-ref combinators) around your handler.

HttpServerResponse values are immutable. A response is a status, optional statusText, headers, cookies, and an HttpBody. Constructors set practical defaults — empty is 204, redirect is 302, and body constructors are 200 — and reflect body metadata (content type, content length) into headers.

import { HttpServerResponse } from "effect/unstable/http"
// 200 text/plain
const ok = HttpServerResponse.text("hello")
// 201 with a header, then a cookie (unsafe variant throws on invalid cookies)
const created = HttpServerResponse.text("created", { status: 201 }).pipe(
HttpServerResponse.setHeader("x-request-id", "abc"),
HttpServerResponse.setCookieUnsafe("seen", "1", { path: "/" })
)
// 302 redirect
const away = HttpServerResponse.redirect("/login")

Most constructors accept an Options object:

interface Options {
readonly status?: number | undefined
readonly statusText?: string | undefined
readonly headers?: Headers.Input | undefined
readonly cookies?: Cookies.Cookies | undefined
readonly contentType?: string | undefined
readonly contentLength?: number | undefined
}

Body-aware constructors narrow this: Options.WithContent omits contentType/contentLength (the body decides them), and Options.WithContentType omits only contentLength.

An empty response, defaulting to status 204.

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.empty() // => 204, no body
HttpServerResponse.empty({ status: 304 }) // => 304

A text/plain response from a string.

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.text("pong")
// => 200, content-type: text/plain

A JSON response built in Effect; serialization failures become HttpBodyError.

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.json({ ok: true })
// => Effect<HttpServerResponse, HttpBodyError>

A JSON response built synchronously; throws if JSON.stringify fails.

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.jsonUnsafe({ ok: true })
// => HttpServerResponse (200, application/json)

Returns a JSON-response constructor that first encodes the value with a schema. Can fail with HttpBodyError on encoding or serialization errors.

import { Schema } from "effect"
import { HttpServerResponse } from "effect/unstable/http"
const User = Schema.Struct({ id: Schema.Number, name: Schema.String })
const userResponse = HttpServerResponse.schemaJson(User)
userResponse({ id: 1, name: "Ada" }, { status: 201 })
// => Effect<HttpServerResponse, HttpBodyError>

A text/html response. Pass a string for a direct response, or use it as a template tag to render interpolated values (returning an Effect).

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.html("<h1>Hi</h1>") // => HttpServerResponse
HttpServerResponse.html`<h1>${"Ada"}</h1>` // => Effect<HttpServerResponse, ...>

A streaming text/html response from a template, encoding the template as a byte stream and supporting streaming interpolated values.

import { Stream } from "effect"
import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.htmlStream`<ul>${Stream.make("<li>a</li>", "<li>b</li>")}</ul>`
// => Effect<HttpServerResponse, never, ...>

A response whose body is raw bytes (defaults to application/octet-stream).

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.uint8Array(new Uint8Array([1, 2, 3]))
// => 200, content-type: application/octet-stream

A response with a platform-native body value (a Web Response, Blob, or ReadableStream) passed through to the adapter.

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.raw(new Blob(["data"]), { contentType: "text/plain" })

A streaming response from a Stream<Uint8Array, E>.

import { Stream } from "effect"
import { HttpServerResponse } from "effect/unstable/http"
const bytes = Stream.make(new Uint8Array([72, 105])) // "Hi"
HttpServerResponse.stream(bytes, { contentType: "text/plain" })

A response whose body is a Web FormData value (the runtime supplies the multipart boundary).

import { HttpServerResponse } from "effect/unstable/http"
const fd = new FormData()
fd.append("name", "Ada")
HttpServerResponse.formData(fd)

A response encoded as application/x-www-form-urlencoded.

import { HttpServerResponse, UrlParams } from "effect/unstable/http"
HttpServerResponse.urlParams(UrlParams.fromInput({ q: "effect", page: 1 }))
// => body: "q=effect&page=1"

A streamed file response for a filesystem path. Requires HttpPlatform; can fail with PlatformError. Supports offset, bytesToRead, and chunkSize.

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.file("/var/www/report.pdf", { contentType: "application/pdf" })
// => Effect<HttpServerResponse, PlatformError, HttpPlatform>

A streamed file response for a Web File-like value. Requires HttpPlatform.

import { HttpServerResponse } from "effect/unstable/http"
declare const file: File
HttpServerResponse.fileWeb(file)
// => Effect<HttpServerResponse, never, HttpPlatform>

Returns a response with a new status code (and optional status text).

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.text("nope").pipe(HttpServerResponse.setStatus(403, "Forbidden"))
// => status 403, statusText "Forbidden"

Set one header, or set many at once.

import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.empty().pipe(
HttpServerResponse.setHeader("cache-control", "no-store"),
HttpServerResponse.setHeaders({ "x-a": "1", "x-b": "2" })
)

Replace the response body. Content-type / content-length headers follow the new body’s metadata.

import { HttpBody, HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.empty().pipe(
HttpServerResponse.setBody(HttpBody.text("replaced"))
)

Cookies are managed on the response’s cookie collection. The safe variants return an Effect failing with CookiesError; the *Unsafe variants throw.

import { Effect } from "effect"
import { Cookies, HttpServerResponse } from "effect/unstable/http"
const withCookies = Effect.gen(function* () {
let res = HttpServerResponse.empty()
// setCookie / setCookieUnsafe: add one cookie
res = yield* HttpServerResponse.setCookie(res, "session", "abc123", {
httpOnly: true,
sameSite: "lax",
path: "/"
})
res = HttpServerResponse.setCookieUnsafe(res, "theme", "dark")
// setCookies / setCookiesUnsafe: add many at once
res = yield* HttpServerResponse.setCookies(res, [
["a", "1"],
["b", "2", { path: "/" }]
])
res = HttpServerResponse.setCookiesUnsafe(res, [["c", "3"]])
// expireCookie / expireCookieUnsafe: emit an already-expired cookie
res = yield* HttpServerResponse.expireCookie(res, "session", { path: "/" })
res = HttpServerResponse.expireCookieUnsafe(res, "theme")
// collection-level operations
res = HttpServerResponse.removeCookie(res, "a") // drop one cookie
res = HttpServerResponse.mergeCookies(res, Cookies.empty) // merge in a collection
res = HttpServerResponse.replaceCookies(res, Cookies.empty) // replace all
res = HttpServerResponse.updateCookies(res, (c) => Cookies.remove(c, "b")) // edit via fn
return res
})
CombinatorEffectful?Description
setCookieyesAdd one cookie, validating it
setCookieUnsafenoAdd one cookie, throwing on invalid input
setCookiesyesAdd many cookies from [name, value, options?] tuples
setCookiesUnsafenoAdd many cookies, throwing on invalid input
expireCookieyesAdd an expired cookie (empty value, Max-Age=0)
expireCookieUnsafenoExpire a cookie, throwing on invalid input
removeCookienoRemove one cookie from the collection
mergeCookiesnoMerge another Cookies collection in
replaceCookiesnoReplace the whole cookie collection
updateCookiesnoMap the cookie collection with a function

toWeb produces a Web Response (cookies become Set-Cookie headers). fromWeb wraps a Web Response. toClientResponse / fromClientResponse bridge to the client model.

import { HttpServerResponse } from "effect/unstable/http"
const res = HttpServerResponse.text("hi")
const web = HttpServerResponse.toWeb(res) // => globalThis.Response
const back = HttpServerResponse.fromWeb(web) // => HttpServerResponse

isHttpServerResponse is the runtime guard.

HttpServerRespondable.Respondable lets a value describe its own response. Domain errors and API errors commonly implement it so error handling can ask the value how to render itself instead of building a response at every call site.

import { Effect } from "effect"
import { HttpServerRespondable, HttpServerResponse } from "effect/unstable/http"
class NotFoundError {
// The protocol method returns Effect<HttpServerResponse, unknown>
[HttpServerRespondable.symbol]() {
return Effect.succeed(HttpServerResponse.text("not found", { status: 404 }))
}
}
HttpServerRespondable.isRespondable(new NotFoundError()) // => true

Converts a Respondable (or an existing HttpServerResponse) into a response. Conversion failures become defects.

import { HttpServerRespondable } from "effect/unstable/http"
declare const value: HttpServerRespondable.Respondable
HttpServerRespondable.toResponse(value)
// => Effect<HttpServerResponse>

The module also exposes toResponseOrElse and toResponseOrElseDefect, which fall back to a supplied response for unknown failures — mapping SchemaError to 400 and NoSuchElementError to 404.

HttpBody is the transport-facing body representation shared by requests and responses. You rarely build one directly (the response constructors do it for you), but the constructors are useful with setBody and when proxying. Variants: Empty, Raw, Uint8Array, FormData, Stream.

The singleton empty body.

import { HttpBody } from "effect/unstable/http"
HttpBody.empty._tag // => "Empty"

UTF-8 text body, defaulting to text/plain.

import { HttpBody } from "effect/unstable/http"
HttpBody.text("hello").contentType // => "text/plain"

JSON body in Effect; JSON.stringify failures become HttpBodyError.

import { HttpBody } from "effect/unstable/http"
HttpBody.json({ a: 1 }) // => Effect<HttpBody.Uint8Array, HttpBodyError>

JSON body built synchronously; throws on serialization failure.

import { HttpBody } from "effect/unstable/http"
HttpBody.jsonUnsafe({ a: 1 }).contentType // => "application/json"

Byte-array body, defaulting to application/octet-stream.

import { HttpBody } from "effect/unstable/http"
HttpBody.uint8Array(new Uint8Array([1, 2])).contentLength // => 2

Wraps an arbitrary runtime body value with optional content metadata.

import { HttpBody } from "effect/unstable/http"
HttpBody.raw(new Blob(["x"]), { contentType: "text/plain", contentLength: 1 })

Streaming body from a Stream<Uint8Array>, defaulting to application/octet-stream; content length is optional.

import { Stream } from "effect"
import { HttpBody } from "effect/unstable/http"
HttpBody.stream(Stream.make(new Uint8Array([1])), "text/plain")

Wraps a Web FormData value as a body (content type left unset for the boundary).

import { HttpBody } from "effect/unstable/http"
const fd = new FormData()
fd.append("name", "Ada")
HttpBody.formData(fd)

Builds a FormData body from a record. Arrays append repeated keys, primitives are stringified, File/Blob values are appended directly, nullish values are skipped.

import { HttpBody } from "effect/unstable/http"
HttpBody.formDataRecord({ name: "Ada", tags: ["a", "b"], skip: null })
// => FormData with name=Ada, tags=a, tags=b

application/x-www-form-urlencoded body from UrlParams.

import { HttpBody, UrlParams } from "effect/unstable/http"
HttpBody.urlParams(UrlParams.fromInput({ q: "effect" }))
// => contentType: application/x-www-form-urlencoded

Stream a file from a path. file stats the file to set content length; fileFromInfo reuses already-known File.Info. Both require FileSystem and may fail with PlatformError.

import { HttpBody } from "effect/unstable/http"
HttpBody.file("/tmp/data.bin")
// => Effect<HttpBody.Stream, PlatformError, FileSystem>

Empty, Raw, Uint8Array, FormData, and Stream are the body variant classes (each carries a _tag). isHttpBody is the runtime guard. HttpBodyError is a tagged error with a reason of JsonError or SchemaError.

import { HttpBody } from "effect/unstable/http"
const body = HttpBody.text("hi")
HttpBody.isHttpBody(body) // => true
body._tag // => "Uint8Array"

Headers is an immutable, lowercase-keyed string map. Names are normalized because HTTP header names are case-insensitive, and combinators return new collections.

import { Option } from "effect"
import { Headers } from "effect/unstable/http"
const h = Headers.fromInput({
"Content-Type": "application/json",
accept: ["application/json", "text/plain"]
})
Headers.get(h, "content-type") // => Option.some("application/json")
h["accept"] // => "application/json, text/plain" (array values joined with ", ")

The empty header collection.

import { Headers } from "effect/unstable/http"
Headers.empty // => {} (Headers)

Builds Headers from a record or iterable of entries; names are lowercased, array values joined with ", ", undefined omitted.

import { Headers } from "effect/unstable/http"
Headers.fromInput({ "X-Trace": "1", drop: undefined })["x-trace"] // => "1"

Treats an existing record as Headers without normalizing names. Caller must supply lowercase keys.

import { Headers } from "effect/unstable/http"
Headers.fromRecordUnsafe({ "content-type": "text/plain" })

Reads a header value safely (lowercases the lookup).

import { Headers } from "effect/unstable/http"
Headers.get(Headers.fromInput({ a: "1" }), "A") // => Option.some("1")

Returns whether a header is present.

import { Headers } from "effect/unstable/http"
Headers.has(Headers.fromInput({ a: "1" }), "A") // => true

Returns a new collection with one header set.

import { Headers } from "effect/unstable/http"
Headers.set(Headers.empty, "Authorization", "Bearer x")["authorization"]
// => "Bearer x"

Sets many headers at once (input normalized via fromInput).

import { Headers } from "effect/unstable/http"
Headers.setAll(Headers.empty, { "X-A": "1", "X-B": "2" })

Merges two Headers; the second wins on conflicts.

import { Headers } from "effect/unstable/http"
Headers.merge(Headers.fromInput({ a: "1" }), Headers.fromInput({ a: "2", b: "3" }))
// => { a: "2", b: "3" }

Removes one header by name.

import { Headers } from "effect/unstable/http"
Headers.remove(Headers.fromInput({ a: "1", b: "2" }), "A") // => { b: "2" }

Removes several headers by name.

import { Headers } from "effect/unstable/http"
Headers.removeMany(Headers.fromInput({ a: "1", b: "2", c: "3" }), ["a", "c"])
// => { b: "2" }

redact returns a record with selected header values wrapped in Redacted. CurrentRedactedNames is the context reference listing which names are masked on inspection (defaults: authorization, cookie, set-cookie, x-api-key).

import { Headers } from "effect/unstable/http"
Headers.redact(Headers.fromInput({ authorization: "secret", a: "1" }), "authorization")
// => { authorization: Redacted(<redacted>), a: "1" }

Equivalence compares two Headers by names and values; HeadersSchema is the schema encoding Headers as a string record; isHeaders is the runtime guard.

import { Headers } from "effect/unstable/http"
Headers.isHeaders(Headers.empty) // => true

UrlParams is an ordered list of [key, value] string pairs that preserves duplicate keys and order. Used for query strings and URL-encoded bodies.

An empty UrlParams.

import { UrlParams } from "effect/unstable/http"
UrlParams.empty.params // => []

Builds from ordered string pairs as-is (no coercion).

import { UrlParams } from "effect/unstable/http"
UrlParams.make([["a", "1"], ["a", "2"]]) // keeps both "a" pairs

Builds from a record, iterable, or URLSearchParams. Primitives become strings, arrays become repeated params, nested records use bracket notation, undefined omitted.

import { UrlParams } from "effect/unstable/http"
UrlParams.toString(UrlParams.fromInput({ a: 1, tags: ["x", "y"] }))
// => "a=1&tags=x&tags=y"

Replaces all values for a key with a single value (appended at the end).

import { UrlParams } from "effect/unstable/http"
UrlParams.toString(UrlParams.set(UrlParams.fromInput({ a: "1" }), "a", "2"))
// => "a=2"

Replaces values for the keys present in the input; other params are preserved.

import { UrlParams } from "effect/unstable/http"
UrlParams.toString(UrlParams.setAll(UrlParams.fromInput({ a: "1", b: "2" }), { a: "9" }))
// => "a=9&b=2"

Appends a value without removing existing values for the key.

import { UrlParams } from "effect/unstable/http"
UrlParams.toString(UrlParams.append(UrlParams.fromInput({ a: "1" }), "a", "2"))
// => "a=1&a=2"

Appends all params from the input, preserving existing ones.

import { UrlParams } from "effect/unstable/http"
UrlParams.toString(UrlParams.appendAll(UrlParams.fromInput({ a: "1" }), { b: "2" }))
// => "a=1&b=2"

Removes all values for a key.

import { UrlParams } from "effect/unstable/http"
UrlParams.toString(UrlParams.remove(UrlParams.fromInput({ a: "1", b: "2" }), "a"))
// => "b=2"

Read all values for a key, or the first / last value as an Option.

import { UrlParams } from "effect/unstable/http"
const p = UrlParams.fromInput({ a: ["1", "2"] })
UrlParams.getAll(p, "a") // => ["1", "2"]
UrlParams.getFirst(p, "a") // => Option.some("1")
UrlParams.getLast(p, "a") // => Option.some("2")

Build a record; single-value keys map to a string, repeated keys to a non-empty array.

import { UrlParams } from "effect/unstable/http"
UrlParams.toRecord(UrlParams.fromInput({ a: 1, e: [1, 2, 3] }))
// => { a: "1", e: ["1", "2", "3"] }

Serializes to a query string (no leading ?).

import { UrlParams } from "effect/unstable/http"
UrlParams.toString(UrlParams.fromInput({ q: "a b" })) // => "q=a+b"

Builds a URL from a base string plus params and an optional hash; returns a Result failing with UrlParamsError.

import { UrlParams } from "effect/unstable/http"
UrlParams.makeUrl("https://x.dev/search", UrlParams.fromInput({ q: "effect" }), undefined)
// => Result.success(URL "https://x.dev/search?q=effect")

Maps the underlying ordered pairs.

import { UrlParams } from "effect/unstable/http"
UrlParams.transform(
UrlParams.fromInput({ a: "1" }),
(pairs) => [...pairs, ["b", "2"]]
)

schemaRecord decodes UrlParams into a record schema; schemaJsonField reads one field’s value as JSON. Both compose with Schema.decodeTo.

import { Schema } from "effect"
import { UrlParams } from "effect/unstable/http"
const toStruct = UrlParams.schemaRecord.pipe(
Schema.decodeTo(Schema.Struct({ some: Schema.String, number: Schema.FiniteFromString }))
)
Schema.decodeSync(toStruct)(UrlParams.fromInput({ some: "value", number: 42 }))
// => { some: "value", number: 42 }

Equivalence, UrlParamsSchema, UrlParamsError, isUrlParams

Section titled “Equivalence, UrlParamsSchema, UrlParamsError, isUrlParams”

Equivalence is order-sensitive; UrlParamsSchema encodes UrlParams as an array of string tuples; UrlParamsError is the tagged error from makeUrl; isUrlParams is the guard.

import { UrlParams } from "effect/unstable/http"
UrlParams.isUrlParams(UrlParams.empty) // => true

The Url module keeps the WHATWG URL as the representation and adds safe parsing plus immutable, pipeable setters. Setters never mutate the input; each clones the URL and assigns on the clone.

Parses a URL string safely, returning a Result (fails with IllegalArgumentError). Accepts an optional base for relative URLs.

import { Url } from "effect/unstable/http"
Url.fromString("/path", "https://example.com")
// => Result.success(URL "https://example.com/path")

Applies a callback to a clone of the URL, allowing several assignments at once.

import { Url } from "effect/unstable/http"
Url.mutate(new URL("https://example.com"), (url) => {
url.username = "user"
url.password = "pass"
}).toString()
// => "https://user:pass@example.com/"

Reads the query parameters as UrlParams.

import { Url } from "effect/unstable/http"
Url.urlParams(new URL("https://example.com?foo=bar")).params
// => [["foo", "bar"]]

Replaces the query parameters with the given UrlParams.

import { Url, UrlParams } from "effect/unstable/http"
Url.setUrlParams(new URL("https://example.com?foo=bar"), UrlParams.fromInput({ key: "value" })).toString()
// => "https://example.com/?key=value"

Reads the params, applies a function, and writes them back.

import { Url, UrlParams } from "effect/unstable/http"
Url.modifyUrlParams(new URL("https://example.com?foo=bar"), UrlParams.append("key", "value")).toString()
// => "https://example.com/?foo=bar&key=value"

Pipeable setters for each URL component. setPassword also accepts Redacted input (the resulting URL still contains the real credential).

import { Url } from "effect/unstable/http"
const base = new URL("https://example.com")
Url.setHash(base, "#section") // url.hash
Url.setHost(base, "api.example.com:8080") // host (domain + port)
Url.setHostname(base, "api.example.com") // domain only
Url.setHref(base, "https://other.dev/") // replace whole URL
Url.setPassword(base, "pass") // password (string | Redacted)
Url.setPathname(base, "/v1/users") // path
Url.setPort(base, 8080) // port (string | number)
Url.setProtocol(base, "http:") // protocol
Url.setSearch(base, "?q=1") // query string
Url.setUsername(base, "user") // username

Cookies is an immutable collection keyed by cookie name (at most one cookie per name). A Cookie stores both the decoded value and the encoded valueEncoded.

import { Cookies } from "effect/unstable/http"
const cookies = Cookies.setUnsafe(Cookies.empty, "session", "abc123", {
httpOnly: true,
path: "/",
sameSite: "lax",
secure: true
})
Cookies.toSetCookieHeaders(cookies)
// => ["session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax"]

The empty cookie collection.

import { Cookies } from "effect/unstable/http"
Cookies.isEmpty(Cookies.empty) // => true

fromIterable / fromReadonlyRecord / fromSetCookie

Section titled “fromIterable / fromReadonlyRecord / fromSetCookie”

Build a collection from an iterable of Cookie, a record keyed by name, or a set of Set-Cookie header strings.

import { Cookies } from "effect/unstable/http"
Cookies.fromSetCookie(["a=1; Path=/", "b=2"])
// => Cookies containing a and b

Create a single Cookie, validating name, value, domain, path, and finite max-age. makeCookie returns a Result; the unsafe variant throws.

import { Cookies } from "effect/unstable/http"
Cookies.makeCookie("a", "1", { path: "/" })
// => Result.success(Cookie)
Cookies.makeCookieUnsafe("a", "1") // => Cookie (throws on invalid input)

Create and add a cookie by name and value. set returns a Result; setUnsafe throws.

import { Cookies } from "effect/unstable/http"
Cookies.set(Cookies.empty, "a", "1") // => Result.success(Cookies)
Cookies.setUnsafe(Cookies.empty, "a", "1") // => Cookies

Create and add many cookies from [name, value, options?] tuples. setAll returns the first CookiesError on failure; setAllUnsafe throws.

import { Cookies } from "effect/unstable/http"
Cookies.setAll(Cookies.empty, [["a", "1"], ["b", "2", { path: "/" }]])
// => Result.success(Cookies)

Add already-built Cookie values (one, or an iterable).

import { Cookies } from "effect/unstable/http"
const a = Cookies.makeCookieUnsafe("a", "1")
Cookies.setCookie(Cookies.empty, a) // add one Cookie
Cookies.setAllCookie(Cookies.empty, [a]) // add many Cookies

Look up a Cookie (or its decoded value) by name as an Option.

import { Cookies } from "effect/unstable/http"
const c = Cookies.setUnsafe(Cookies.empty, "a", "1")
Cookies.get(c, "a") // => Option.some(Cookie)
Cookies.getValue(c, "a") // => Option.some("1")

Combine two collections; the second collection wins on name conflicts.

import { Cookies } from "effect/unstable/http"
Cookies.merge(
Cookies.setUnsafe(Cookies.empty, "a", "1"),
Cookies.setUnsafe(Cookies.empty, "b", "2")
)

Remove a cookie by name.

import { Cookies } from "effect/unstable/http"
Cookies.remove(Cookies.setUnsafe(Cookies.empty, "a", "1"), "a")
// => empty collection

Add an expired cookie (empty value, Max-Age=0, epoch Expires). expireCookie returns a Result; the unsafe variant throws.

import { Cookies } from "effect/unstable/http"
Cookies.expireCookieUnsafe(Cookies.empty, "session", { path: "/" })
// => Set-Cookie: session=; Max-Age=0; Path=/; Expires=...

toCookieHeader / toSetCookieHeaders / toRecord

Section titled “toCookieHeader / toSetCookieHeaders / toRecord”

toCookieHeader builds an outbound request Cookie header. toSetCookieHeaders builds response Set-Cookie header strings. toRecord returns decoded values keyed by name.

import { Cookies } from "effect/unstable/http"
const c = Cookies.setAllUnsafe(Cookies.empty, [["a", "1"], ["b", "2"]])
Cookies.toCookieHeader(c) // => "a=1; b=2"
Cookies.toSetCookieHeaders(c) // => ["a=1", "b=2"]
Cookies.toRecord(c) // => { a: "1", b: "2" }

Parses a request Cookie header into a record of name/value pairs.

import { Cookies } from "effect/unstable/http"
Cookies.parseHeader("a=1; b=2") // => { a: "1", b: "2" }

Serializes a single Cookie into its Set-Cookie string.

import { Cookies } from "effect/unstable/http"
Cookies.serializeCookie(Cookies.makeCookieUnsafe("a", "1", { path: "/", httpOnly: true }))
// => "a=1; Path=/; HttpOnly"

Schema transforming Cookies into a record of decoded string values keyed by name.

import { Cookies } from "effect/unstable/http"
Cookies.schemaRecord // => Schema decoding Cookies → Record<string, string>

isCookie / isCookies / isEmpty & CookiesError

Section titled “isCookie / isCookies / isEmpty & CookiesError”

isCookie / isCookies are runtime guards; isEmpty checks for no cookies; CookiesError is the tagged error (reason._tag is InvalidCookieName, InvalidCookieValue, InvalidCookieDomain, InvalidCookiePath, or CookieInfinityMaxAge).

import { Cookies } from "effect/unstable/http"
Cookies.isCookies(Cookies.empty) // => true
Cookies.isEmpty(Cookies.empty) // => true