npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@i.un/api-client

v2.0.2

Published

Universal API client for i.un services

Readme

Api Client

基于 ofetch 的轻量 HTTP 客户端,可选用 OpenAPI spec 驱动端到端类型。 A lightweight HTTP client built on ofetch, with optional end-to-end types driven by your OpenAPI spec.

  • 自动注入 Authorization token / Automatic Authorization token injection
  • 单飞(single-flight)的可选 token 刷新 / Optional single-flight token refresh
  • 统一的成功 / 错误处理,协议可配 / Unified success / error handling, configurable protocol
  • 可选:用 openapi-typescript 的 paths 做端到端类型 —— endpoint 补全、query / body / path 参数与响应全部从 spec 推导 / Optional: end-to-end types from an openapi-typescript paths — endpoint autocomplete, query / body / path params and responses all inferred
  • 还内置:请求链编排(chain)、Protobuf 安全传输 / Also bundled: request-chain orchestration, Protobuf secure transport

浏览器、Node、浏览器扩展、Cloudflare Workers 均可用。 Works in browsers, Node, browser extensions, and Cloudflare Workers.


安装 / Installation

npm install @i.un/api-client
# or
pnpm add @i.un/api-client
# or
yarn add @i.un/api-client

包名 / Package name: @i.un/api-client


快速上手 / Quick Start

import { createApiClient, type TokenStorage } from "@i.un/api-client";

// 1. 提供 TokenStorage(token 从哪读 / 存到哪)
//    Provide a TokenStorage (where to read / store the token)
const tokenStorage: TokenStorage = {
  async getAccessToken() {
    return localStorage.getItem("access_token") || "";
  },
  async setAccessToken(token: string) {
    localStorage.setItem("access_token", token);
  },
};

// 2. 创建 client(不带 spec:响应为 unknown,用 <T> 指定,兼容旧用法)
//    Create a client (no spec: responses are unknown; pass <T> — legacy-compatible)
const client = createApiClient({
  baseURL: "https://api.example.com",
  tokenStorage,
  refreshToken: "/auth/refresh", // token 刷新端点 / token-refresh endpoint
});

// 3. 调用(两段式:url + options)/ Call it (two-arg: url + options)
const user = await client.get<{ name: string }>("/user/profile");
const updated = await client.post<{ ok: boolean }>("/user/profile", {
  body: { name: "foo" },
});

2.0 起接口为两段式 (url, options) —— query / path / body 都放进 optionsSince 2.0 the shape is two-arg (url, options)query / path / body all live in options. 见下方「迁移」/ see "Migration" below.


用 OpenAPI spec 强类型 / Typed against your OpenAPI spec

传入 openapi-typescript 生成的 paths,即可获得端到端类型:endpoint 自动补全、query / path / body 与响应全部从 spec 推导,无需手写类型或 <T>。 Pass the paths produced by openapi-typescript to get end-to-end types: endpoint autocomplete, plus query / path / body and responses inferred from the spec — no hand-written types or <T>.

import { createApiClient } from "@i.un/api-client";
import type { paths } from "./api"; // openapi-typescript 生成 / generated by openapi-typescript

// 默认:通用库,如实返回后端响应(不解包)
const api = createApiClient<paths, "/api">({
  baseURL: "https://api.example.com/api",
  tokenStorage,
});

const me = await api.get("/me");                          // 响应 = 后端原样返回
const u  = await api.get("/users/{id}", { path: { id } }); // path 参数类型安全 + 运行时拼接
await api.post("/users", { body: { name: "foo" } });       // body 类型来自 spec
await api.get("/users", { query: { page: 1 } });           // query 类型来自 spec

// 信封后端({ code, data, message }):显式 opt-in 解包,自动剥出 data
// Envelope backend: opt in to unwrapping, the data field is peeled automatically
const envApi = createApiClient<paths, "/api", "data">({
  baseURL: "https://api.example.com/api",
  tokenStorage,
  dataKey: "data",
});
const user = await envApi.get("/me");                      // 响应 = 解包后的 data

三个泛型轴 / The three generic axes

createApiClient<Paths, Prefix, DataKey>

| 泛型 / Generic | 含义 / Meaning | 默认 / Default | | --- | --- | --- | | Paths | openapi-typescript 的 paths;unknown = 未类型化(回退到 <T>(url))/ the paths; unknown = untyped | unknown | | Prefix | 从 spec key 剥掉的前缀,对应 baseURL 的路径段 / prefix stripped from spec keys, matching the baseURL path segment | '' | | DataKey | never(默认)= 不解包,如实返回;传字段名(如 'data')= 解包该字段 / never (default) = no unwrap; a field name = unwrap it | never |

例:baseURLhttps://x.com/api、spec key 为 /api/me,则 Prefix='/api',调用传去前缀的 /me。 e.g. with baseURL https://x.com/api and spec key /api/me, set Prefix='/api' and call /me.

默认不解包 —— api-client 是通用库,不假设后端协议。要解包 { code, data, message } 信封,显式传 DataKey 字段名 + 运行时 dataKey(类型会强制二者一致:自定义字段名时 dataKey 必填且必须 === DataKey)。 No unwrap by default — api-client is a universal client and assumes nothing about your backend. To unwrap a { code, data, message } envelope, pass a DataKey field name plus the runtime dataKey (the type enforces they match: with a custom field name dataKey is required and must === DataKey).

生成 spec / Generate the spec

openapi-typescript https://api.example.com/api/docs-json -o ./api.ts

在项目侧组装短别名 / Build project-side aliases

类型内核来自 @i.un/openapi-kit,已由本包原样再导出(ResponseFor / QueryFor / BodyFor / AllPaths / MethodsFor / DefaultMethod / ApiKit …)。你可以在项目里组装短别名: The type core comes from @i.un/openapi-kit and is re-exported as-is here (ResponseFor / QueryFor / BodyFor / AllPaths / MethodsFor / DefaultMethod / ApiKit …). Assemble short aliases in your project:

import type {
  ResponseFor, BodyFor, QueryFor, AllPaths, MethodsFor, DefaultMethod,
} from "@i.un/api-client";
import type { paths } from "./api";

type Pfx = "/api";
type DKey = "data";

export type ApiResponse<
  P extends AllPaths<paths, Pfx>,
  M extends MethodsFor<paths, Pfx, P> = DefaultMethod<paths, Pfx, P>,
> = ResponseFor<paths, Pfx, P, M, unknown, DKey>;

export type ApiBody<
  P extends AllPaths<paths, Pfx>,
  M extends MethodsFor<paths, Pfx, P> = DefaultMethod<paths, Pfx, P, "post">,
> = BodyFor<paths, Pfx, P, M>;

// 用法 / usage:
type Me = ApiResponse<"/me">;
type CreateUser = ApiBody<"/users", "post">;

不传泛型时,createApiClient(...) 完全退回未类型化模式:get<T>(url) 照旧可用。 Without generics, createApiClient(...) falls back to untyped mode: get<T>(url) works as before.


响应处理:默认不解包 / Response: no unwrap by default

默认,api-client 如实返回后端响应(裸数据、完整信封、null / 204 都原样返回) —— 它是通用库,不假设你的后端协议。 By default api-client returns the backend response as-is (raw data, full envelope, null / 204 alike) — it's a universal client and assumes nothing about your protocol.

信封解包是 opt-in。 如果后端用如下信封 / Unwrapping is opt-in. If your backend uses this envelope:

export interface ApiResult<T> {
  code: number;
  data: T;
  message: string;
}

传字段名开启解包 / pass the field name to enable unwrapping:

const api = createApiClient<paths, "/api", "data">({ baseURL, tokenStorage, dataKey: "data" });

开启后 / Once enabled:

  • code === 0(成功)→ 返回 data 字段。/ on success → returns the data field.
  • code !== 0(业务失败)→ 抛出 ApiError(见下)。/ on business failure → throws ApiError (see below).
  • 字段名与成功码都可配:codeKey / messageKey / dataKey / successCode(默认 { code, data, message } + 0)。/ field names & success code are configurable.

要整个 client 都拿完整信封,就不开解包(默认);要换整套解包/错误逻辑见「高级」。 To get full envelopes everywhere, just don't enable unwrapping (the default); to replace the whole unwrap / error logic see "Advanced".


错误类型 / Error Type

export interface ApiError extends Error {
  code: number;   // 业务错误码 / business error code
  data?: unknown;  // 后端 data 字段 / backend data field
  status?: number; // HTTP 状态码(网络层错误)/ HTTP status (network-level)
}

export const isApiError = (error: unknown): error is ApiError =>
  error instanceof Error && "code" in error;
try {
  const data = await api.get("/some/api");
} catch (e) {
  if (isApiError(e)) {
    console.log("业务错误 / business error", e.code, e.message, e.data);
  } else {
    console.error("意外错误 / unexpected error", e);
  }
}

配置项 / Options: CreateApiClientOptions

export interface CreateApiClientOptions {
  baseURL: string;

  // 可选。不传则不自动注入 Authorization、也不自动刷新 —— 适合无状态 / token 由
  // onRequest 自行注入的场景(如 Cloudflare Worker 透传用户 token)。
  // Optional. Omit for stateless / token-injected-in-onRequest setups (e.g. a
  // Cloudflare Worker forwarding the user's token): no auto-inject, no auto-refresh.
  tokenStorage?: TokenStorage;

  // 可选:自动 token 刷新 / optional automatic token refresh
  refreshToken?: (() => Promise<string>) | string | false;

  // 鉴权错误码,默认 isAuthError 据此判定(code 或 status 命中即触发刷新),默认 401
  // auth code for the default isAuthError (code or status match → refresh), default 401
  unauthorizedCode?: number;

  // 如何判定鉴权错误(默认 code/status === unauthorizedCode)/ how to detect an auth error
  isAuthError?: (error: { code: number; status?: number }) => boolean;

  // 信封字段名与成功码(解包模式用)。dataKey 不在此 —— 由 DataKey 泛型 + DataKeyOption 控制
  // envelope field names & success code (unwrap mode). dataKey lives on the DataKey generic.
  codeKey?: string;
  messageKey?: string;
  successCode?: number;

  // 自定义成功解包 / custom success unwrapping
  unwrapResponse?<T>(result: unknown): T;

  // 自定义错误映射 / custom error mapping
  createErrorFromResult?(res: unknown): Error;

  // 请求 / 响应钩子(如 Protobuf 编解码)/ request / response hooks (e.g. Protobuf)
  onRequest?: (context: FetchContext) => void | Promise<void>;
  onResponse?: (context: FetchContext) => void | Promise<void>;
}

TokenStorage

export interface TokenStorage {
  getAccessToken: () => Promise<string> | string;
  setAccessToken: (token: string) => Promise<void> | void;
}

token 存哪由你决定 / You decide where the token lives:

  • 浏览器 / Browser:localStorage / sessionStorage / cookies
  • Node:内存 / Redis / 数据库 / in-memory, Redis, DB …
  • 扩展 / Extension:chrome.storage、cookies …

tokenStorage 可选:无状态场景(如 Cloudflare Worker 代用户转发请求)可以不传,改在 onRequest 里自行注入 token —— 库就不碰 token、不刷新,适合「每请求透传当前用户 token」且不希望有任何跨请求 token 状态的用法。 tokenStorage is optional: for stateless setups (e.g. a Worker forwarding a user's request), omit it and inject the token yourself in onRequest. The library then never touches/refreshes the token — ideal for per-request token forwarding with zero cross-request state.


自动刷新 Token / Automatic Token Refresh

方式一:字符串 endpoint(推荐)/ Option 1: String endpoint (recommended)

const api = createApiClient({ baseURL, tokenStorage, refreshToken: "/auth/refresh" });

行为 / Behavior:

  • 命中 isAuthError(默认 401)时,POST 刷新端点,取 data.access_token 存回 tokenStorage,再自动重试一次原请求。 On an auth error (401 by default), POSTs the refresh endpoint, stores data.access_token, then retries the original request once.
  • 单飞 / Single-flight:内部 refreshingPromise 保证同一时刻只有一个刷新请求,并发的 401 复用其结果。 An internal refreshingPromise ensures only one in-flight refresh; concurrent 401s reuse it.

方式二:自定义函数 / Option 2: Custom function

const api = createApiClient({
  baseURL,
  tokenStorage,
  refreshToken: async () => {
    const res = await ofetch<{ accessToken: string }>("/auth/refresh", { baseURL, method: "POST" });
    return res.accessToken;
  },
});

建议不要refreshToken 里再调用会自动刷新的 request,以免刷新端点本身返回鉴权错误时递归。 Prefer not to call the auto-refreshing request inside refreshToken, to avoid recursion if the refresh endpoint itself returns an auth error.


请求方法 / Request Methods

const { rawRequest, request, get, post, put, patch, del } = createApiClient(...);

| 方法 / Method | 说明 / Description | | --- | --- | | rawRequest | 底层 $fetch 实例,原样返回,无刷新 / 解包 / underlying $fetch, raw, no refresh / unwrap | | request<T>(url, options?) | 核心逃生舱:token 注入 + 刷新 + 解包 + 抛错 / core escape hatch | | get / del (url, options?) | options.query 作查询参数 / options.query as query string | | post / put / patch (url, options?) | options.body 作请求体 / options.body as body |

options 同时透传 ApiRequestOptions(headers / timeout / responseType …);带 {param} 的端点 options.path 必填options also passes through ApiRequestOptions (headers / timeout / responseType …); for endpoints with {param}, options.path is required.

// GET /users?page=1
await get("/users", { query: { page: 1 } });

// POST /users { name }
await post("/users", { body: { name: "foo" } });

// GET /users/123  ——  模板 + path,运行时拼接 / template + path, interpolated at runtime
await get("/users/{id}", { path: { id: "123" } });

// 直接用底层 request(更灵活)/ raw request (more flexible)
const data = await request<User>("/users/1", { method: "GET" });

高级:自定义解包与错误映射 / Advanced: Custom unwrapping and error mapping

后端不是 { code, data, message } 协议时,可覆盖 / If your backend differs from { code, data, message }:

const api = createApiClient({
  baseURL,
  tokenStorage,
  refreshToken: false,

  unwrapResponse<T>(result) {
    // 例:后端返回 { success, result, errorMsg } / e.g. backend returns { success, result, errorMsg }
    if (result && typeof result === "object" && "success" in result) {
      const body = result as any;
      if (body.success) return body.result as T;
    }
    return result as T;
  },

  createErrorFromResult(res): Error {
    const body = res as any;
    return new Error(body.errorMsg || "Request failed");
  },
});

请求链执行 / Request Chain Execution

executeRequestChain 用于编排多个有依赖的 HTTP 请求:共享 Context、支持 变量替换、可按策略 智能路由。这是运行时动态能力,与上面的 spec 类型无关。 executeRequestChain orchestrates multiple dependent HTTP requests: a shared Context, variable substitution, and strategy-based smart routing. It is a runtime/dynamic feature, independent of the spec types above.

核心概念 / Core Concepts

  • Rule (ChainRequestRule):链中的一步 / a single step.
  • Context:累积结果的共享对象,每步可读写 / shared object accumulating results.
  • Handler (NetworkHandler):按 URL / Method 决定用哪个 Adapter 执行 / picks the Adapter per URL / Method.

规则配置 / Rule Configuration (ChainRequestRule)

| 字段 / Property | 类型 / Type | 说明 / Description | | :-- | :-- | :-- | | key | string | 结果存入 Context 的键 / key to store the result in Context | | request | HttpRequestSpec | method / url / headers / body | | selector | string | 可选,点号路径(如 user.id)提取数据 / optional dot-path to extract data | | optional | boolean | 为真时该步失败也继续 / continue even if this step fails |

变量替换 / Variable Substitution

可在 url / headers / body{{key.path}},引擎以 Context 中的值替换。 Use {{key.path}} in url / headers / body; the engine substitutes from the Context.

示例:三步编排 / Example: 3-step orchestration

import { executeRequestChain } from "@i.un/api-client";

const rules = [
  { key: "auth", request: { method: "POST", url: "/login", body: { user: "admin" } } },
  {
    key: "profile",
    request: { method: "GET", url: "/users/{{auth.userId}}", headers: { "X-Session": "{{auth.token}}" } },
    selector: "data",
  },
  {
    key: "update",
    request: { method: "PUT", url: "/sync", body: { source: "web", profile: "{{profile}}" } },
  },
];

const result = await executeRequestChain(rules);

自定义路由(策略模式)/ Custom Routing (Strategy Pattern)

提供一组 NetworkHandler 把请求路由到不同环境(如把 chrome-extension:// 走扩展 API,其余走标准 fetch)。 Provide NetworkHandlers to route requests to different environments (e.g. chrome-extension:// to an extension API, the rest to standard fetch).

const handlers = [
  {
    name: "extension-router",
    shouldHandle: (url) => url.startsWith("chrome-extension://"),
    adapter: {
      post: (url, body) => chrome.runtime.sendMessage({ url, body }),
      get: (url) => /* ... */,
    },
  },
];

await executeRequestChain(rules, handlers);

Protobuf 与安全通信 / Protobuf & Security

Protobuf 模块在 ofetch 之上提供安全层:二进制序列化、XOR 混淆、通过 hooks 自动集成。 The Protobuf module adds a security layer over ofetch: binary serialization, XOR obfuscation, automatic integration via hooks.

处理流程 / Pipeline

ObjectTransform(可选)→ Protobuf EncodeXOR ObfuscationBinary Payload

集成 Hook / Integration Hooks

createProtobufHooks 给 client 加 Protobuf 支持 / Use createProtobufHooks to add Protobuf support:

import { createApiClient, createProtobufHooks, createXorObfuscator } from "@i.un/api-client";

const hooks = createProtobufHooks({
  // obfuscator: createXorObfuscator("my-secret-key"), // 可选 / optional
  transform: {
    beforeEncode: (data) => ({ ...data, ts: Date.now() }),
    afterDecode: (res) => res.data,
  },
});

const api = createApiClient({ baseURL: "/api", tokenStorage, ...hooks });

// 用法:把 Content-Type / Accept 设为 application/x-protobuf
// usage: set Content-Type / Accept to application/x-protobuf
await api.post("/secure-endpoint", {
  body: { foo: "bar" },
  headers: { "Content-Type": "application/x-protobuf" },
});

手动配置请求头 / Manual Header Configuration

import { getProtobufHeaders } from "@i.un/api-client";

// 收发都用 Protobuf(默认)/ send & receive Protobuf (default)
const headers = { ...getProtobufHeaders({ send: true, receive: true }), "X-Custom": "val" };

// 仅接收 / receive only
const receiveOnly = getProtobufHeaders({ send: false, receive: true });

await api.get("/data", { headers: receiveOnly });

信封模式 vs 自定义类型 / Envelope Mode vs Custom Type Mode

  • 信封模式(默认)/ Envelope (default):不提供 protoType 时用内置 SecurePayload(时间戳 + JSON 串)。无需为每个接口定义 schema 即可隐藏 DevTools 数据。 Without a protoType, uses the built-in SecurePayload (timestamp + JSON). Hides data from DevTools without per-API schemas.
  • 自定义类型 / Custom Type:提供编译自 .protoprotoType,获得完整二进制性能与类型安全。 Provide a protoType compiled from .proto for full binary performance and type safety.
const hooks = createProtobufHooks({ protoType: MyCompiledMessage });

自定义混淆 / Custom Obfuscator

默认不混淆。用内置 XOR / Off by default. Built-in XOR:

import { createProtobufHooks, createXorObfuscator } from "@i.un/api-client";

const hooks = createProtobufHooks({ obfuscator: createXorObfuscator("your-secret-key") });

或完全自定义 / or fully custom:

import { createProtobufHooks, type ProtobufObfuscator } from "@i.un/api-client";

const myObfuscator: ProtobufObfuscator = {
  encrypt: (data: Uint8Array) => data, // 你的加密 / your encryption
  decrypt: (data: Uint8Array) => data, // 你的解密 / your decryption
};

const hooks = createProtobufHooks({ obfuscator: myObfuscator });

服务端 / 边缘 用法 / Server-Side Usage (Node / Workers)

import { parseSecureRequest, secureResponse, createXorObfuscator } from "@i.un/api-client";

// 1. 解析进来的请求 / parse incoming request
const body = await parseSecureRequest(request, { obfuscator: createXorObfuscator("key") });

// 2. 返回安全响应 / send secure response
return await secureResponse({ success: true }, { obfuscator: createXorObfuscator("key") });

从 1.x 迁移 / Migration from 1.x

2.0 把接口从三段式改为两段式(query / body / pathoptions),以对齐 OpenAPI spec 类型。 2.0 moves from a three-arg to a two-arg shape (query / body / path go into options), to align with OpenAPI spec types.

// 1.x → 2.0
get("/list", { page: 1 })        →  get("/list", { query: { page: 1 } })
post("/x", body)                 →  post("/x", { body })
del("/x", { id })                →  del("/x", { query: { id } })
get(`/u/${id}`)                  →  get("/u/{id}", { path: { id } })
get<T>("/me")                    →  get<T>("/me")              // 单参调用不变 / unchanged

仅传 url 的调用(get<T>(url))无需改动;只有「第二参直接传 query / body 对象」的写法需要包成 { query } / { body }。 Single-arg calls (get<T>(url)) are unchanged; only "second arg is a raw query / body object" needs wrapping in { query } / { body }.

另外两个破坏性变更 / Two more breaking changes:

  • 默认不再解包。 1.x 默认解包 { code, data, message }data;2.0 默认如实返回。信封后端要显式开启:createApiClient<paths, '/api', 'data'>({ …, dataKey: 'data' })No unwrap by default. 1.x unwrapped data; 2.0 returns as-is. Envelope backends opt in: createApiClient<paths, '/api', 'data'>({ …, dataKey: 'data' }).
  • 移除 returnFullResponse 它改运行时返回却不改类型(类型对不上)。要完整信封就用默认(不解包)的 client;要解包后的 data 就开 dataKeyreturnFullResponse removed. It changed the runtime return without changing the type. For full envelopes use a default (no-unwrap) client; for unwrapped data enable dataKey.

环境与注意事项 / Environment & Notes

  • 基于 ofetch,可用于浏览器、Node 18+、Nuxt、Workers 等 / Built on ofetch; browsers, Node 18+, Nuxt, Workers.
  • 依赖 fetch / Headers:浏览器与 Node 18+ 内置;更老的 Node 需 undici / cross-fetch polyfill。 Requires fetch / Headers: built-in in browsers and Node 18+; polyfill older Node with undici / cross-fetch.
  • 核心 client 不依赖 window / localStorage / chrome;环境相关逻辑放进你的 TokenStorage。 The core client does not depend on window / localStorage / chrome; keep env-specific logic in your TokenStorage.

License

MIT