@spilne/perfect-http
v0.2.0
Published
HTTP client on the Perfect effect runtime — typed errors, retries, streaming (SSE/NDJSON), mockable transport.
Readme
@spilne/perfect-http
Typed-effect HTTP client for @spilne/perfect-core. Three tiers of fetch, a
configurable client with middleware, retry with full outcome control, native
streaming (text / lines / NDJSON / SSE), typed error response bodies, and a
test-double mock — every request returns Eff<A, Throws<HttpClientError>>,
so failures are visible in the type and handled with .catch / .catchTag.
Install
bun add @spilne/perfect-httpNot yet published to npm — install from the workspace for now.
Quickstart
import { succeed } from "@spilne/perfect-core";
import { DefaultHttpClient, type ResponseParser } from "@spilne/perfect-http";
interface User {
id: number;
name: string;
}
// Any { safeParse } object works — zod schemas satisfy this directly.
const UserSchema: ResponseParser<User> = {
safeParse: (d: unknown) =>
d !== null &&
typeof d === "object" &&
"id" in d &&
"name" in d &&
typeof d.id === "number" &&
typeof d.name === "string"
? { success: true, data: { id: d.id, name: d.name } }
: { success: false, error: "not a User" },
};
const client = new DefaultHttpClient({
baseUrl: "https://api.example.com",
headers: { authorization: "Bearer xyz" },
});
// fetch → status check → JSON → schema, as one typed effect
const user = await client.get("/users/1", UserSchema).orDie().run();
// errors are tagged — handle them in the type
const safe = client
.get("/users/1", UserSchema)
.catchTag("HttpStatusError", (e) => succeed({ id: -1, name: `(status ${e.status})` }));client.withOverrides({ headers: { "x-trace": "t-123" } }) derives a client —
headers spread-merge, everything else falls back to the base.
Three tiers of fetch
Below the client sit three free functions, composing upward:
| Tier | Function | Adds |
| ---- | ----------------------------------------------------- | ---------------------------------------------- |
| 1 | httpFetch | raw Response through a transport — no checks |
| 2 | httpFetchOk | status check → HttpStatusError on non-2xx |
| 3 | httpRequest / httpRequestJson / httpRequestText | body decode + schema parse |
Every request flows through an HttpTransport (default: globalThis.fetch) —
pass your own to mock, proxy, or instrument.
Features
- Typed errors —
HttpNetworkError,HttpTimeoutError,HttpStatusError,HttpParseError,HttpUnknownError;HTTP_RETRYABLElists the transient tags - Client —
DefaultHttpClientwith baseUrl, default headers,.get/.post/.put/.patch/.delete,withOverrides,HttpMiddlewarehooks (onRequest/onResponse/onError) - DI —
HttpClientServicetag for Layer-based injection - Retry —
withRetryAll,withRetryAllBy, andretryHttpfor HTTP-ready transient defaults;Retrynamespace mirrors these asRetry.all,Retry.allBy, andRetry.http; for polling use core's.repeatUntil/.repeatUntilWithBackoff. - Streaming —
httpStreambase plushttpStreamText/httpStreamLines/httpStreamNDJSON/httpStreamSSE, and composableparseSSE/parseNDJSONpipes - Testing —
MockHttpClient/mockHttpClient: route-matched test double with call recording
Links
- Repo: https://github.com/spilne/perfect
- Full guide:
documentation/13-http.md - Runnable examples:
examples/ - OpenTelemetry tracing for this client:
@spilne/perfect-http-otel
