@okfetch/fetch
v0.5.0
Published
A type-safe fetch wrapper with validation, retries, plugins, and streaming.
Maintainers
Readme
@okfetch/fetch
@okfetch/fetch is the transport core of the okfetch ecosystem.
It wraps the standard fetch API with:
Result-based error handling viabetter-result- Standard Schema-compatible response validation
- request retries and timeouts
- auth helpers
- lifecycle plugins
- SSE-style stream parsing
If you want typed HTTP calls without building a full generated client, this is the package to start with.
Installation
bun add @okfetch/fetch better-resultnpm install @okfetch/fetch better-resultWhat It Exports
Functions:
okfetchvalidateClientErrorsvalidateAllErrors
Errors:
FetchErrorTimeoutErrorApiErrorParseErrorValidationErrorPluginError
Types:
OkfetchOptionsOkfetchErrorOkfetchPluginOkfetchPluginHooksOkfetchSuccessRetryOptionsAuth
Quick Example
import { okfetch } from "@okfetch/fetch";
import { z } from "zod/v4";
const todoSchema = z.object({
completed: z.boolean(),
id: z.number(),
title: z.string(),
userId: z.number(),
});
const result = await okfetch("https://jsonplaceholder.typicode.com/todos/1", {
outputSchema: todoSchema,
});
result.match({
err: (error) => console.error(error._tag, error.message),
ok: (todo) => console.log(todo.title),
});Request Options
okfetch(url, options) accepts standard fetch options plus okfetch-specific behavior:
outputSchemaapiErrorDataSchemabaseURLparamsquerybodyauthtimeoutstreamvalidateOutputshouldValidateErrorpluginsretryfetch
Any schema object that implements Standard Schema v1 can be used for outputSchema and apiErrorDataSchema.
URL building
You can pass a fully qualified URL directly, or combine a relative path with baseURL.
Path params are replaced from params, and query strings are built from query.
await okfetch("/users/:id", {
baseURL: "https://api.example.com",
method: "GET",
params: { id: 42 },
query: { include: ["teams", "profile"] },
});Validation
outputSchema validates successful responses:
await okfetch("https://api.example.com/me", {
outputSchema: z.object({
id: z.string(),
email: z.string().email(),
}),
});Providing apiErrorDataSchema parses structured error bodies into ApiError.data, validated against the schema by default:
await okfetch("https://api.example.com/me", {
apiErrorDataSchema: z.object({
code: z.string(),
message: z.string(),
}),
});shouldValidateError limits validation to specific status codes. Statuses it excludes still get the JSON-parsed body on ApiError.data, just without validation — the schema then only types the data. When validation runs and fails, the raw parsed body is kept on data instead of being dropped.
import { validateClientErrors } from "@okfetch/fetch";
await okfetch("https://api.example.com/me", {
apiErrorDataSchema: z.object({
code: z.string(),
message: z.string(),
}),
// only validate 4xx responses; 5xx bodies are parsed but not validated
shouldValidateError: validateClientErrors,
});Retries
Supported strategies:
fixedlinearexponential
await okfetch("https://api.example.com/me", {
retry: {
attempts: 3,
initialDelay: 100,
strategy: "exponential",
},
});Auth
Built-in auth shapes:
{ type: "basic", username, password }{ type: "bearer", token }{ type: "custom", prefix, value }
await okfetch("https://api.example.com/me", {
auth: {
type: "bearer",
token: process.env.API_TOKEN ?? "",
},
});Plugins
Plugins are a core extension point.
import type { OkfetchPlugin } from "@okfetch/fetch";
const plugin: OkfetchPlugin = {
name: "timing",
version: "1.0.0",
hooks: {
onRequest(context) {
console.log("->", context.method, context.url.toString());
return context;
},
onFail(_context, _response, error) {
console.error(error._tag, error.message);
},
},
};Available lifecycle hooks:
initonRequestonResponseonSuccessonFailonRetry
onFail runs for every failure after the request context is built: transport, timeout, API, parse and validation errors, plus errors thrown by onRequest or onResponse hooks and failures reading the response body. init failures happen before a request context exists and are returned without calling onFail.
Streaming
Set stream: true to receive a ReadableStream.
const result = await okfetch("https://example.com/events", {
stream: true,
outputSchema: z.object({
id: z.number(),
message: z.string(),
}),
});When an outputSchema is present, each SSE data: chunk is parsed and validated independently.
Error Handling
okfetch never throws expected HTTP, parsing, timeout, or validation failures from the request API itself. Instead it returns tagged errors inside the Result.
Use match when you want explicit branching:
result.match({
err: (error) => {
if (error._tag === "ApiError") {
console.error(error.statusCode, error.data);
}
},
ok: (data) => console.log(data),
});Related Packages
@okfetch/apibuilds typed API clients on top of this package@okfetch/loggerprovides a ready-made logging plugin
