English | 中文
@unimolecule/oh-my-fetch is a general-purpose Fetch client built on top of ky.
It keeps the core small, safe, and runtime-neutral, then layers optional behavior
through explicit plugins.
client:HttpClient, helper methods, plugin orchestration, scoped cleanup.core: body, query, headers, response parsing, and ky option conversion.lifecycle: disposable request scopes, abort signal composition, dedupe maps.security: safe JSON parsing and prototype-pollution sanitization.plugins: opt-in policies such as business status, request formatting, and error reporting.errors:HttpRequestError, redaction, and ky/fetch error normalization.validation: pluggable schema validation for request and response data.
The core does not know about application UI, business response wrappers, logging providers, or framework-specific exceptions.
- JSON responses are parsed with
jsonSecurity: "strict". - Business wrapper checks are not enabled by default.
- Request body trimming/date formatting is not enabled by default.
- Schema validation runs only when
bodySchemaorresponseSchemais supplied. - Dedupe is disabled unless
dedupeis set. HttpRequestError.toJSON()omits raw response data and request body values.
import { createHttpClient } from "@unimolecule/oh-my-fetch/client";
const api = createHttpClient({
prefix: "/api",
timeout: 30 * 1000,
headers: {
accept: "application/json",
},
});
type User = {
id: string;
name: string;
};
const user = await api.get<User>("users/current");Absolute URL inputs bypass prefix and baseUrl, which lets one configured
client call external services without creating a second client.
await api.get("https://api.example.com/status");The root entry exports only the core client, error class, and shared types. Plugins and specialized helpers are imported from explicit subpaths.
| Entrypoint | Use for |
|---|---|
@unimolecule/oh-my-fetch |
core client, HttpRequestError, types |
@unimolecule/oh-my-fetch/client |
createHttpClient, HttpClient |
@unimolecule/oh-my-fetch/errors |
HttpRequestError and error types |
@unimolecule/oh-my-fetch/status |
HTTP status message helpers |
@unimolecule/oh-my-fetch/validation |
schema validation helpers |
@unimolecule/oh-my-fetch/json |
secure JSON parsing helpers |
@unimolecule/oh-my-fetch/dedupe |
request dedupe lifecycle helpers |
@unimolecule/oh-my-fetch/plugins/business-status |
business wrapper failure plugin |
@unimolecule/oh-my-fetch/plugins/error-reporter |
error reporting plugin |
@unimolecule/oh-my-fetch/plugins/request-format |
request body formatting plugin |
@unimolecule/oh-my-fetch/plugins/validation |
request/response schema validation plugin |
@unimolecule/oh-my-fetch/utils |
lower-level shared utility types/helpers |
Use query for URL parameters and body for request payloads. The client picks
json, body, or urlencoded transport fields for ky.
await api.get<User[]>("users", {
query: {
page: 1,
roles: ["admin", "editor"],
},
});
await api.post("users", {
name: " Ada ",
});Bodies are not mutated or normalized by default. Add the request-format plugin when a service wants recursive string trimming and Date formatting.
import { createHttpClient } from "@unimolecule/oh-my-fetch/client";
import { requestFormatPlugin } from "@unimolecule/oh-my-fetch/plugins/request-format";
const formattedApi = createHttpClient({
plugins: [requestFormatPlugin()],
});The default response type is json.
const text = await api.get<string>("health", {
responseType: "text",
});
const response = await api.get<Response>("download", {
responseType: "response",
});When responseType: "response" is used, the returned Response has data and
a redacted config attached.
Validation is built in but lazy. It runs only when schemas are supplied.
Adapters, Standard Schema, Zod-like safeParse, Yup-like validate, and
functions are supported.
const user = await api.post(
"users",
{ id: "1" },
{
bodySchema: (value) => ({
...(value as Record<string, unknown>),
id: Number((value as { id: string }).id),
}),
responseSchema: (value) => value as User,
},
);Validation failures throw HttpRequestError with kind set to
request_validation or response_validation.
Plugins are explicit lifecycle policies.
import { createHttpClient } from "@unimolecule/oh-my-fetch/client";
import { businessStatusPlugin } from "@unimolecule/oh-my-fetch/plugins/business-status";
import { errorReporterPlugin } from "@unimolecule/oh-my-fetch/plugins/error-reporter";
import { requestFormatPlugin } from "@unimolecule/oh-my-fetch/plugins/request-format";
import { validationPlugin } from "@unimolecule/oh-my-fetch/plugins/validation";
const api = createHttpClient({
plugins: [
validationPlugin(),
requestFormatPlugin(),
businessStatusPlugin(),
errorReporterPlugin({
report: (error) => {
console.error(error);
},
}),
],
});businessStatusPlugin() treats common wrappers like { success: false } or
{ type: "error" } as failures. A custom validator replaces the default.
const api = createHttpClient({
plugins: [
businessStatusPlugin({
validator: (data) => {
const result = data as { ok?: boolean; code?: string };
if (result.ok === false) {
return {
failed: true,
code: result.code,
status: 409,
message: "Custom failure",
data,
};
}
return false;
},
}),
],
});Error reporting is a plugin, not a core callback.
const api = createHttpClient({
plugins: [
errorReporterPlugin({
report: (error, context) => {
console.error(error.message, context.config);
},
}),
],
});Set dedupe: true to abort an older equivalent in-flight GET/HEAD request.
Use a string to dedupe any method by a custom key.
await api.get("users/current", { dedupe: true });
await api.post("search", body, { dedupe: "search:current" });Dispose the client to abort pending dedupe-managed requests and release plugin resources.
api.dispose();jsonSecurity controls prototype-pollution sanitization:
"strict"recursively removes__proto__,constructor, andprototype."shallow"removes those keys only at the top level."off"leaves parsed JSON untouched.
const api = createHttpClient({
defaults: {
jsonSecurity: "strict",
},
});All normalized failures use HttpRequestError.
import { HttpRequestError } from "@unimolecule/oh-my-fetch/errors";
try {
await api.get("users/current");
} catch (error) {
if (error instanceof HttpRequestError) {
console.error(error.kind, error.status, error.config);
}
}Stable kind values:
http_statustimeoutnetworkabortbusinessrequest_validationresponse_validationunknown
Request config stored on errors is redacted. Sensitive headers and query values
such as authorization, cookie, token, password, and API keys are replaced with
[redacted]; request bodies are never stored raw in config.
The package works in runtimes with Fetch-compatible globals: browsers,
Cloudflare Workers, Node, and serverless isolates. Blob, FormData,
ReadableStream, AbortController, and upload/download progress support follow
the host runtime and ky behavior.