@cplieger/fetch

中文文档
2.3.0发布于 2026/11/01Apache-2.0TypeScriptES Module零依赖构建来源已验证近 30 天下载 3950

Handle TypeScript JSON requests without throws, as typed results or data-or-null values

此软件包已被标记为弃用

Moved into @cplieger/ui: import @cplieger/ui/fetch. Guide: https://github.com/cplieger/ui/blob/main/docs/migrating.md

原始使用文档
文档目录12 节
  1. Why use it
  2. Install
  3. Usage
  4. API
  5. Errors are values
  6. Paths stay under your base URL
  7. Unsupported by design
  8. Related projects
  9. Documentation
  10. Contributing
  11. Disclaimer
  12. License

fetch

npm JSR Mutation (TS)

@cplieger/fetch wraps the platform fetch for TypeScript so JSON requests never throw. Each call returns a typed success or error value, or only the data or null when that is all you need.

It replaces the try/catch, the response.ok check and the JSON.parse you would otherwise write around each call. It has no runtime dependencies and ships as ESM-only TypeScript source, which your bundler compiles with your own code. It needs TypeScript 5.0 or later and runs on Node 18, Chrome 103, Safari 16 and Firefox 100 or later. It is licensed under Apache-2.0.

Why use it

@cplieger/fetch is built for front-end and server-rendered code that calls a JSON API and wants to handle every failure in one if.

  • A network failure, a timeout, a cancelled request, a non-2xx response and a body that fails validation all come back as an error value with a code.
  • A failed response keeps its status, headers and parsed JSON body, so a 429's Retry-After is one property away.
  • Validation is a plain function, so a zod or valibot schema plugs in as (v) => schema.parse(v).
  • With an absolute base URL, an absolute or protocol-relative path cannot reach another origin.
  • It is about 2 kB minified and gzipped, and its tests run against Chromium's own fetch, Headers and AbortSignal.

Consider ky if you want retries, hooks and upload and download progress in one client. Consider ofetch if you need streamed responses and automatic retries on Node, browsers and workers.

Install

npx jsr add @cplieger/fetch
# or
npm i @cplieger/fetch

Usage

Create one instance per backend, then branch on the result:

import { createFetch } from "@cplieger/fetch";

export const api = createFetch({ baseUrl: "https://api.example.com/v1" });

const res = await api.apiGetRaw<{ id: string; name: string }>("/users/me");
if (res.ok) {
  console.log(res.data.name, res.headers.get("ETag"));
} else {
  // status is 0 when no response arrived; code says why.
  console.error(res.status, res.code, res.error);
}

When you only need the data, the helpers without a suffix return it, or null on any error. apiPost, apiPut and apiPatch send their second argument as JSON with Content-Type: application/json:

const user = await api.apiGet<{ id: string; name: string }>("/users/me"); // the user, or null
const created = await api.apiPost<{ id: string }>("/items", { name: "widget" });

Validate a response body with any function that returns the typed value or throws. A throw becomes an error with code: "decode":

import { z } from "zod";

const User = z.object({ id: z.string(), name: z.string() });
const user = await api.apiGetTyped("/users/me", (v) => User.parse(v)); // typed, or null

Every helper takes a last options argument for a cancel signal, a timeout, extra headers, a decoder, a pre-encoded body, skipping the success body, or reading it as a Blob:

const controller = new AbortController();
await api.apiGetRaw("/slow", { signal: controller.signal, timeoutMs: 5_000 });
await api.apiDeleteRaw("/items/1", { ignoreBody: true });
const poster = await api.apiGet("/posters/42", { as: "blob" }); // a Blob, or null

To act on every failure in one place, such as sending the user to sign in when a session expires, give the instance an onError hook. It sees each failure with its code already read from the body, and nothing it throws or returns reaches the caller:

export const api = createFetch({
  baseUrl: "https://api.example.com/v1",
  onError: (err) => {
    if (err.status === 401 && err.code === "session_expired") location.assign("/login");
  },
});

Put query parameters in the path, such as /items?page=2. To add a header that changes after startup, such as a token, read it inside the instance's prepareHeaders hook, which runs on every request. The timeout starts after prepareHeaders returns, so a hook that can hang needs its own limit. Requests, timeouts and runtimes covers every setting and option.

API

  • Instance: createFetch(config?) returns a FetchInstance. FetchConfig holds baseUrl, credentials, prepareHeaders, fetchFn, maxResponseBytes and onError.
  • Requests on an instance: requestRaw and request, plus apiGet, apiPost, apiPut, apiPatch and apiDelete in a plain form that returns the data or null and a *Raw form that returns the full result. apiGetTyped and apiPostTyped also take a decoder.
  • Timeout: withTimeout(signal, ms) and API_TIMEOUT_MS, 30,000 ms.
  • Types: ApiOk<T>, ApiErr, ApiResult<T>, Decoder<T>, FailedRequest, HttpMethod, RequestOptions<T> for JSON reads and BlobRequestOptions for as: "blob".

The full reference is on JSR.

Errors are values

requestRaw and every *Raw helper resolve to ApiResult<T> and never throw. A success is { ok: true, status, data, headers }. An error is { ok: false, status, error, code?, requestId?, headers?, body? }.

status is 0 when no response arrived. code is then network, timeout, cancelled or invalid, where invalid means the request could not be built and was never sent. A 2xx body that is not JSON or fails the decoder is decode, with the real status. A non-2xx response keeps its status and headers. When its body is JSON, body holds it, and its error, code and request_id fields fill the result's error, code and requestId.

The server controls code and can send timeout or another library code. A library code comes with status 0, or the 2xx status for decode, and a server code always comes with its non-2xx status. Treat body as untrusted input.

A 204 or an empty 2xx body gives data: undefined, which the plain helpers turn into null. A JSON null, 0, false or "" is real data.

Results and errors has the full contract.

Paths stay under your base URL

With an absolute baseUrl, every request goes to that scheme and host. A path such as https://other.example or //host becomes part of the path, so https://api.example.com/v1 plus https://other.example requests https://api.example.com/v1/https://other.example. A .., a dot segment or a backslash cannot climb out of the base path, and the query string and fragment are sent as written.

The protection needs a baseUrl with a scheme and host. With an empty or relative baseUrl, a protocol-relative path can still reach another origin. With no baseUrl, the path goes to fetch() unchanged, so never pass untrusted input as the whole path.

Base URLs and untrusted responses covers this and the response size cap.

Unsupported by design

@cplieger/fetch has no retries or backoff, interceptor chains, response caching, decoder combinators, changeable or global settings, automatic idempotency-key or request-ID headers, or streamed responses. Unsupported by design gives the reason for each and what to use instead.

  • @cplieger/actions builds on this library and adds retry with backoff, request dedupe, optimistic updates and notifications.
  • httpx is the Go library for outbound HTTP calls, with retries and backoff built in.

Documentation

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

Apache-2.0. See LICENSE.

版本与发布记录

@cplieger/fetch 共发布 14 个版本,首次发布于 2026/07/05;过去 12 个月发布了 14 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.3.0latest2026/11/019 天前
2.3.0-dev.1dev2026/10/1823 天前
2.2.22026/10/081 个月前
2.2.12026/09/201 个月前
2.2.02026/09/141 个月前
2.1.22026/08/232 个月前

关于 @cplieger/fetch 的常见问题

@cplieger/fetch 是什么?

Handle TypeScript JSON requests without throws, as typed results or data-or-null values注意:该包已被作者标记为弃用——Moved into @cplieger/ui: import @cplieger/ui/fetch. Guide: https://github.com/cplieger/ui/blob/main/docs/migrating.md

如何安装 @cplieger/fetch?

在项目目录运行 npm install @cplieger/fetch,或使用 pnpm add / yarn add / bun add。

@cplieger/fetch 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cplieger/fetch 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cplieger/fetch 可以商用吗?

@cplieger/fetch 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@cplieger/fetch 的下载量有多少?

根据 npm 官方下载统计,@cplieger/fetch 在 2026/10/11 – 2026/11/09 这 30 天内被下载 3,950 次,日均约 132 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cplieger/fetch 还在维护吗?

最新版本 2.3.0 发布于 2026/11/01(9 天前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/07/05。作者已将其标记为弃用,新项目建议寻找替代方案。