soon-fetch
v4.1.0
Published
a request lib alternative to axios with timeout, request reusing, race, response cache ...
Maintainers
Readme
English | 中文 | Installation
soon-fetch
A lightweight http request lib , alternative to axios with timeout, request reusing, race, response cache ...
- 🌐 automatic parse restful api url parameters
- ⭐ rapid define a request api
- 🎯 fork sub-instance with prefix and default options
- ⌛ timeout disconnect
- 📦 request reusing
- 🚀 request race
- 📝 response cache
- 🔤 automatic serialization of JSON
- 📏 .min size less than 10K, smaller after zip
- 💡 smart type tips with Typescript
Example
import { createSoon, soonFetch } from "soon-fetch";
// 使用 soonFetch 作为基础请求函数
const request = async <T>(url: string, options?: SoonOptions): Promise<T> => {
const isGet = !options?.method || options?.method.toLocaleLowerCase() === "get";
const response = await soonFetch({
url,
options,
baseURL: '/api',
baseOptions: {
timeout: 20 * 1000,
headers: new Headers({
Authorization: "Bearer " + localStorage.getItem("token"),
}),
share: isGet ? true : false,
staleTime: isGet ? 2 * 1000 : 0,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<T>;
};
const soon = createSoon(request);
/** GET */
soon.get("/user?id=123");
soon.get("/user", { query: { id: 123 } });
soon.get("/user/:id", { params: { id: 123 } });
/** POST */
soon.post("/login", { body: { username: "admin", password: "123456" } });
/**Define API */
export const login = soon
.POST("/user/login")
.Body<{ username: string; password: string }>()
.Ok<{ token: string }>();
//the develop tools will have type tips for request and response
login({ username: "admin", password: "123" }).then((res) => {
localStorage.setItem("token", res.token);
});Features
Shortcut
soon.get(url, options);
soon.post(url, options);
soon.put(url, options);
soon.patch(url, options);
soon.delete(url, options);
soon.head(url, options);
soon.options(url, options);Restful Url Params
url like /:key , will handle the key
soon.get("/api/user/:id", { params: { id: 1 } });
// api/user/1
soon.get("/api/:job/:year", { params: { job: "engineer", year: 5 } });
//api/engineer/5Timeout
//** the request level timeout, will override the instance level timeout */
soon.get(url, { timeout: 1000 * 20 });Share pending request
If a request is made again before the first completes, will reuse the first request instead of making a new request.
soon.get(url, { share: true });Response cache
A cached response will be returned if the request is made again within the specified time.
soon.get(url, { staleTime: 1000 * 60 * 5 });Request race
If a second request is made before the first completes, abort the first to avoid race conditions from out-of-order responses.
import { useEffect, useRef, useState } from "react";
type User = { name: string; job: string };
const api = soon.GET("/api/users").Query<{ page: number }>().Ok<User[]>();
export default function App() {
const refAbort = useRef<[AbortController] | []>([]);
const [list, setList] = useState<User[]>([]);
const [page, setPage] = useState(1);
useEffect(() => {
api({ page }, { aborts: refAbort.current })
.then(setList)
.catch(console.log);
}, [page]);
return (
<div>
<button onClick={() => setPage((pre) => pre + 1)}>next</button>
<div>
{list.map((item) => (
<div key={item.name}>{item.name}</div>
))}
</div>
</div>
);
}Rapid Define APIs
//可以是 GET POST PATCH PUT DELETE
//GET 请求数据传递至query,其他方法请求数据传递至body
soon.GET(url:string).Query<Query>().Ok<Response>()
soon.POST(url:string).Body<Body>().Ok<Response>()
soon.GET(url:string).Options({ timeout: 5000 }).Ok<Response>()
soon.POST(url:string).Body<Body>().Options({ timeout: 5000 }).Ok<Response>()
//define an api
export const getUserInfo = soon.GET("/user/:id").Ok();
//then use in any where
getUserInfo({ id: 2 }).then((res) => console.log(res));
//define an api with options
export const getUserInfoWithOptions = soon.GET("/user/:id").Options({ timeout: 5000 }).Ok();
//then use in any where
getUserInfoWithOptions({ id: 2 }).then((res) => console.log(res));
//with typescript,
export const login = soon
.POST("/user/login")
.Body<{ username: string; password: string }>()
.Ok<{ token: string }>();
//the develop tools will have type tips for request and response
login({ username: "admin", password: "123" }).then((res) => {
localStorage.setItem("token", res.token);
});
//with Params for type safety
export const getUserById = soon.GET("/user/:id").Params<{ id: number }>().Ok<{ id: number; name: string }>();
getUserById({ id: 1 }).then((res) => console.log(res));API
SoonOptions
// function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
// RequestInit is fetch's init options
type SoonOptions = Omit<RequestInit, "body"> & {
body?: RequestInit["body"] | object;
query?:
| Record<
string,
| string
| number
| boolean
| null
| undefined
| (string | number | boolean | null | undefined)[]
>
| URLSearchParams;
params?: Record<string, string | number>;
timeout?: number;
aborts?: AbortController[] | never[];
share?: boolean;
staleTime?: number;
};createSoon
Create a soon request instance.
Parameters:
request: A function to handle the actual request, receives url and options, returns a Promise
Returns: An object containing request method, API methods (GET, POST, PUT, DELETE, PATCH), shortcut methods (get, post, put, delete, patch, head, options), and fork method for creating sub-instances
Example:
import { createSoon, soonFetch } from "soon-fetch";
// Define custom request wrapper with business logic
const request = async <T>(url: string, options?: SoonOptions): Promise<T> => {
const isGet = !options?.method || options?.method.toLowerCase() === "get";
const response = await soonFetch({
url,
options,
baseURL: '/api',
baseOptions: {
timeout: 20 * 1000,
headers: new Headers({
Authorization: "Bearer " + localStorage.getItem("token"),
}),
share: isGet,
staleTime: isGet ? 2 * 1000 : 0,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<T>;
};
// Create soon instance
const soon = createSoon(request);
// Usage 1: Shortcut methods with generics
const users = await soon.get<{ id: number; name: string }[]>("/api/users");
// Usage 2: Define typed APIs with chain calls
export const getUserById = soon
.GET("/user/:id")
.Params<{ id: number }>()
.Ok<{ id: number; name: string }>();
export const createUser = soon
.POST("/user")
.Body<{ name: string; email: string }>()
.Ok<{ id: number }>();
// Usage 3: Use defined APIs
const user = await getUserById({ id: 1 });
const newUser = await createUser({ name: "John", email: "[email protected]" });fork
Create a sub-instance with a URL prefix and default options.
Parameters:
prefix: URL prefix string (e.g. "/api/v1")defaultOptions?: Default options that apply to all requests via this sub-instance
Returns: A new createSoon instance with the prefix and default options pre-applied.
Example:
// Create a forked sub-instance for a specific API version
const v1 = soon.fork("/api/v1", { timeout: 5000 });
const v2 = soon.fork("/api/v2", { timeout: 10000 });
// All requests are automatically prefixed and use default options
v1.get("/users"); // GET /api/v1/users with timeout: 5000
v2.get("/users"); // GET /api/v2/users with timeout: 10000
// Options can still be overridden per request
v1.get("/users", { timeout: 8000 }); // GET /api/v1/users with timeout: 8000
// Chain API definitions work too
const getUser = v1.GET("/users/:id").Params<{ id: number }>().Ok<User>();
getUser({ id: 1 }); // GET /api/v1/users/1 with timeout: 5000
// Fork can be nested
const admin = v1.fork("/admin");
admin.get("/stats"); // GET /api/v1/admin/stats with timeout: 5000
// Absolute URLs bypass the prefix
v1.get("https://other-api.com/data"); // Uses the absolute URL directlycreateShortApi
Factory function to create API shortcut methods. Used to generate type-safe API calling methods, supporting path parameters, query parameters, and request body.
Parameters:
wrapper: Wrapper function to handle actual request logic
Returns: An object containing GET, POST, PUT, DELETE, PATCH and other methods, each method supports chain calling
Example:
const API = createShortApi(
async <T>(url: string, method: string, params: Record<string, string | number> | undefined, query: any, body: any, options: any): Promise<T> => {
// Handle request logic
const { url: _url } = parseUrl(url, { params, query });
const response = await fetch(_url, { ...options, method, body });
return response.json();
}
);
// Usage example
const getUser = API.GET("/api/users/:id").Ok<{ id: number; name: string }>();
const userData = await getUser({ id: 1 });
// With Params for type safety
const getUserById = API.GET("/api/users/:id").Params<{ id: number }>().Ok<{ id: number; name: string }>();
const userData = await getUserById({ id: 1 });createShortMethods
Factory function to create shortcut methods.
Parameters:
methods: HTTP methods arraywrapper: Wrapper function that receives method name and returns a function to process requests
Returns: Shortcut call object containing specified methods
Example:
const methods = createShortMethods(["get", "post"] as const, (method) => {
return async <T>(url: string, options?: SoonOptions): Promise<T> => {
const response = await fetch(url, { ...options, method });
return response.json();
};
});
// Usage: methods.get<{ id: number; name: string }[]>('/api/users')parseOptions
Parse URL options.
Parameters:
urlOptions: Object containing url, options, baseURL and baseOptions
Returns: Object containing parsed url, options, is_body_json, and abortController
Example:
const parsed = parseOptions({
url: "/api/users/:id",
options: { params: { id: "123" } },
baseURL: "https://api.example.com",
});
// Returns: { url: 'https://api.example.com/api/users/123', options: {...}, is_body_json: false, abortController: AbortController }mergeHeaders
Merge multiple Headers objects.
Parameters:
headersList: List of Headers objects to merge
Returns: Merged Headers object, later ones will overwrite earlier ones with the same name
Example:
const headers1: HeadersInit = { "Content-Type": "application/json" };
const headers2: HeadersInit = { Authorization: "Bearer token" };
const mergedHeaders = mergeHeaders(headers1, headers2);mergeSignals
Merge multiple AbortSignal signals.
Parameters:
signals: Array of AbortSignals to mergetimeout: Optional timeout time (milliseconds)
Returns: Merged AbortSignal, any signal termination will trigger termination
Example:
const controller1 = new AbortController();
const controller2 = new AbortController();
const mergedSignal = mergeSignals(
[controller1.signal, controller2.signal],
5000
);parseUrl
Merge URL and its related parameters. Handle baseURL, path parameters and query parameters to generate complete URL.
Parameters:
url: Original URLconfig: Configuration object, including query parameters, path parameters and base URL
Returns: Object containing url (complete URL string), path (URL without query string), and query (URLSearchParams instance)
Example:
const { url } = parseUrl("/api/users/:id", {
params: { id: "123" },
query: { filter: "active" },
baseURL: "https://api.example.com",
});
// Returns: 'https://api.example.com/api/users/123?filter=active'parseQuery2Arr
Parse query parameters into a [key, value][] array.
Supports multiple input formats: string, URLSearchParams, Record, and array-of-tuples.
Parameters:
query: Query parameters in any supported format (string, URLSearchParams, Record, array-of-tuples)
Returns: Array of [key, value] pairs
Example:
parseQuery2Arr("page=1&size=10");
// Returns: [["page", "1"], ["size", "10"]]
parseQuery2Arr({ id: 123, status: "active" });
// Returns: [["id", "123"], ["status", "active"]]
parseQuery2Arr(new URLSearchParams("page=1"));
// Returns: [["page", "1"]]mergeOptions
Merge multiple option objects. Merge request options, including special handling of headers and signals.
Parameters:
optionsList: List of option objects to merge
Returns: Merged option object
Example:
const defaultOptions = {
timeout: 5000,
headers: { "Content-Type": "application/json" },
};
const requestOptions = {
method: "POST",
body: JSON.stringify({ name: "John" }),
};
const mergedOptions = mergeOptions(defaultOptions, requestOptions);isBodyJson
Determine if the request body is a JSON object. Check if body is a plain object, not special types like FormData or Blob.
Parameters:
body: Request body
Returns: Returns true if it is a JSON object, otherwise false
Example:
isBodyJson({ name: "John" }); // true
isBodyJson(new FormData()); // false
isBodyJson("string"); // falsegenRequestKey
Generate a unique identification key for the request (sync). Generate a unique key value based on the request's URL, method, headers, query parameters, etc., used for caching and request sharing.
Note: This is the synchronous version. For body types like
Blob/FormDatathat require async processing (e.g., SHA-256 digest), it falls back to a non-deterministic key (nonce-based) to avoid blocking. UsegenRequestKeyAsyncfor fully deterministic key generation. Note: The SHA-256 digest used bygenRequestKeyAsyncrequires a secure context (HTTPS). Under plain HTTP,crypto.subtle.digestis unavailable, andgenRequestKeyAsyncwill fall back to a non-deterministic key forBlob/ArrayBuffer/DataViewbodies, preventing incorrect cache hits or request sharing.
Parameters:
req: Request object containing url and optionssortObjKeys?: Whether to sort object keys for stable serialization
Returns: The unique identification string of the request
Example:
const key = genRequestKey({
url: "/api/users",
options: { method: "GET", params: { id: 1 } },
});genRequestKeyAsync
Generate a unique identification key for the request (async).
Same as genRequestKey, but supports async digest of Blob/FormData bodies for fully deterministic key generation.
Parameters:
req: Request object containing url and optionssortObjKeys?: Whether to sort object keys for stable serialization
Returns: Promise resolving to the unique identification string of the request
Example:
const key = await genRequestKeyAsync({
url: "/api/users",
options: { method: "GET", params: { id: 1 } },
});createRequestStore
Create request store instance. Provides request caching, sharing, and race condition handling.
Parameters:
options: Configuration optionsmaxCacheSize: Maximum cache size (default: 100000)
Returns: Request store object with the following methods:
entry(key): Get entry for specific request keydispose(): Dispose the store and clear intervalsget(key): Get request by keyset(key, value): Set request by keyremove(key): Remove request by keygetAll(): Get all requestsremoveAll(): Remove all requestsclearCache(): Clear all cacheclearExpiredCache(): Clear expired cacheabortAll(): Abort all requests
Example:
const store = createRequestStore({ maxCacheSize: 1000 });
// Use store in requestsdeepSort
Deep sort object keys. Recursively sort the keys of an object to generate a stable object serialization result.
Parameters:
obj: Object to sortsortArr: Whether to sort arrays (default: false)
Returns: Object with sorted keys
Example:
const obj = { b: 2, a: 1, c: { z: 3, y: 2 } };
const sorted = deepSort(obj);
// Returns: { a: 1, b: 2, c: { y: 2, z: 3 } }createSilentRefresh
Create silent refresh instance. Used to handle silent refresh functionality when token expires.
Parameters:
refresh_token_fn: Function to refresh token
Returns: Function that accepts success and failure callbacks
Example:
const silentRefresh = createSilentRefresh(async () => {
// Refresh token logic
await refreshToken();
});
// Usage example
silentRefresh(
() => console.log("Refresh successful"),
() => console.log("Refresh failed")
);soonFetch
A lightweight fetch wrapper with caching, sharing, and race condition handling.
Parameters:
config: Configuration objecturl: Request URLoptions: Request options (SoonOptions)baseURL: Base URLbaseOptions: Base request optionsstore: Custom request storesortRequestKey: Whether to sort request key
Returns: Promise that resolves to Response
Example:
const response = await soonFetch({
url: "/api/users",
options: {
method: "GET",
query: { page: 1 },
share: true,
staleTime: 5000,
},
baseURL: "https://api.example.com",
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json() as User[];toFormData
Convert object to FormData. Used to convert plain objects to FormData format, supports files and regular values.
Parameters:
body: Object to convert
Returns: Converted FormData object
Example:
const formData = toFormData({
name: "John",
avatar: fileBlob,
age: 30,
});progressDownload
Download with progress tracking. Used to download files with progress updates.
Parameters:
response: Response objectonProgress: Progress callback function
Returns: Promise that resolves to ArrayBuffer
Example:
const response = await fetch("/api/download");
const buffer = await progressDownload(response, (progress, downloaded, total) => {
console.log(`Progress: ${progress}%, Downloaded: ${downloaded}/${total}`);
});progressReadBody
Read response body with progress tracking. Used to read response bodies with progress updates.
Parameters:
body: ReadableStreamonProgress: Progress callback functiontotal: Total size in bytes (default: 0)
Returns: Promise that resolves to ArrayBuffer
Example:
const response = await fetch("/api/download");
const buffer = await progressReadBody(response.body!, (progress, downloaded, total) => {
console.log(`Progress: ${progress}%, Downloaded: ${downloaded}/${total}`);
});requestWithStore
Request wrapper with store support. Used to handle requests with caching, sharing, and race condition handling.
Parameters:
store: Request store instancerequestFn: Request functionrequestKey: Request keyfetchAbort: AbortControlleroptions: Options object
Returns: Promise that resolves to the response
Example:
const store = createRequestStore();
const data = await requestWithStore(store, () => fetch(url, options), requestKey, abortController, {
share: true,
staleTime: 5000,
});English | 中文 | Installation
soon-fetch
极轻量的请求库,不到 10K
- 🌐 自动解析 rest Url 的参数
- ⭐ 快捷定义请求 api
- 🎯 fork 子实例(前缀+默认选项)
- ⌛ 超时断开
- 📦 请求复用
- 🚀 请求竞态
- 📝 响应缓存
- 🔤 自动处理 JSON
- 📏 不到 10K , zip 后会更小
- 💡 用 typescript 有智能类型提醒
示例
import { createSoon, soonFetch } from "soon-fetch";
// 使用 soonFetch 作为基础请求函数
const request = async <T>(url: string, options?: SoonOptions): Promise<T> => {
const isGet = !options?.method || options?.method.toLocaleLowerCase() === "get";
const response = await soonFetch({
url,
options,
baseURL: '/api',
baseOptions: {
timeout: 20 * 1000,
headers: new Headers({
Authorization: "Bearer " + localStorage.getItem("token"),
}),
share: isGet ? true : false,
staleTime: isGet ? 2 * 1000 : 0,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<T>;
};
const soon = createSoon(request);
/** GET */
soon.get("/user?id=123");
soon.get("/user", { query: { id: 123 } });
soon.get("/user/:id", { params: { id: 123 } });
/** POST */
soon.post("/login", { body: { username: "admin", password: "123456" } });
/**定义 API */
export const login = soon
.POST("/user/login")
.Body<{ username: string; password: string }>()
.Ok<{ token: string }>();
//开发工具会有请求和响应的智能提醒
login({ username: "admin", password: "123" }).then((res) => {
localStorage.setItem("token", res.token);
});
//使用 Params 进行类型安全定义
export const getUserById = soon.GET("/user/:id").Params<{ id: number }>().Ok<{ id: number; name: string }>();
getUserById({ id: 1 }).then((res) => console.log(res));特别功能
快捷方法
soon.get(url, options);
soon.post(url, options);
soon.put(url, options);
soon.patch(url, options);
soon.delete(url, options);
soon.head(url, options);
soon.options(url, options);Restful Url 参数自动处理
url 包含 /:key 会解析匹配 key
soon.get("/api/user/:id", { params: { id: 1 } });
// api/user/1
soon.get("/api/:job/:year", { params: { job: "engineer", year: 5 } });
//api/engineer/5超时
//** 请求级超时, 会覆盖实例级超时 */
soon.get(url, { timeout: 1000 * 20 });共享未完成的请求
如果在第一个请求完成之前再次发起相同的请求,则会复用第一个请求,而不是发起新的请求。
soon.get(url, { share: true });响应缓存
如果在指定时间内再次发起相同的请求,则会返回缓存的响应。
soon.get(url, { staleTime: 1000 * 60 * 5 });请求竞态
如果在第一个请求完成之前发起第二个请求,则会中止第一个请求,以避免因响应顺序错乱导致的问题。
import { useEffect, useRef, useState } from "react";
type User = { name: string; job: string };
const api = soon.GET("/api/users").Query<{ page: number }>().Ok<User[]>();
export default function App() {
const refAbort = useRef<[AbortController] | []>([]);
const [list, setList] = useState<User[]>([]);
const [page, setPage] = useState(1);
useEffect(() => {
api({ page }, { aborts: refAbort.current })
.then(setList)
.catch(console.log);
}, [page]);
return (
<div>
<button onClick={() => setPage((pre) => pre + 1)}>next</button>
<div>
{list.map((item) => (
<div key={item.name}>{item.name}</div>
))}
</div>
</div>
);
}快速定义 API
//可以是 GET POST PATCH PUT DELETE
//GET 请求数据传递至query,其他方法请求数据传递至body
soon.GET(url:string).Query<Query>().Ok<Response>()
soon.POST(url:string).Body<Body>().Ok<Response>()
soon.GET(url:string).Options({ timeout: 5000 }).Ok<Response>()
soon.POST(url:string).Body<Body>().Options({ timeout: 5000 }).Ok<Response>()
//定义一个api
export const getUserInfo=soon.GET('/user/:id').Ok()
//使用
getUserInfo({id:2}).then(res=>console.log(res))
//定义一个带选项的api
export const getUserInfoWithOptions=soon.GET('/user/:id').Options({ timeout: 5000 }).Ok()
//使用
getUserInfoWithOptions({id:2}).then(res=>console.log(res))
//用typescript,
export const login=soon
.POST('/user/login')
.Body<{username:string,password:string}>()
.Ok<{token:string}>()
//开发工具会有请求和响应的智能提醒
login({username:'admin',password:'123'}).then(res=>{
localStorage.setItem('token', res.token);
})
//使用 Params 进行类型安全定义
export const getUserById = soon.GET('/user/:id').Params<{ id: number }>().Ok<{ id: number; name: string }>();
getUserById({ id: 1 }).then(res => console.log(res));API
SoonOptions
// function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
// RequestInit 为原生 fetch 的 init 选项
type SoonOptions = Omit<RequestInit, "body"> & {
body?: RequestInit["body"] | object;
query?:
| Record<
string,
| string
| number
| boolean
| null
| undefined
| (string | number | boolean | null | undefined)[]
>
| URLSearchParams;
params?: Record<string, string | number>;
timeout?: number;
aborts?: AbortController[] | never[];
share?: boolean;
staleTime?: number;
};createSoon
创建一个 soon 请求实例。
参数:
request: 用于处理实际请求的函数,接收 url 和 options,返回一个 Promise
返回: 包含 request 方法、API 方法(GET、POST、PUT、DELETE、PATCH)和快捷方法(get、post、put、delete、patch、head、options)以及 fork 方法的对象
示例:
const soon = createSoon(
async (url, options) => {
const response = await fetch(url, options);
return response.json();
}
);
// 使用示例
const data = await soon.get("/api/users");
// 定义带选项的 API
export const login = soon
.POST("/user/login")
.Body<{ username: string; password: string }>()
.Ok<{ token: string }>();
login({ username: "admin", password: "123" }).then((res) => {
localStorage.setItem("token", res.token);
});fork
创建一个带有 URL 前缀和默认选项的子实例。
参数:
prefix: URL 前缀字符串(如 "/api/v1")defaultOptions?: 默认选项,对该子实例的所有请求生效
返回: 一个新的 createSoon 实例,已预置前缀和默认选项。
示例:
// 为不同 API 版本创建 fork 子实例
const v1 = soon.fork("/api/v1", { timeout: 5000 });
const v2 = soon.fork("/api/v2", { timeout: 10000 });
// 所有请求会自动添加前缀并使用默认选项
v1.get("/users"); // GET /api/v1/users with timeout: 5000
v2.get("/users"); // GET /api/v2/users with timeout: 10000
// 单次请求仍可覆盖默认选项
v1.get("/users", { timeout: 8000 }); // GET /api/v1/users with timeout: 8000
// 链式 API 定义同样适用
const getUser = v1.GET("/users/:id").Params<{ id: number }>().Ok<User>();
getUser({ id: 1 }); // GET /api/v1/users/1 with timeout: 5000
// fork 支持嵌套
const admin = v1.fork("/admin");
admin.get("/stats"); // GET /api/v1/admin/stats with timeout: 5000
// 绝对 URL 会绕过前缀
v1.get("https://other-api.com/data"); // 直接使用该 URLcreateShortApi
创建 API 快捷方法的工厂函数。 用于生成类型安全的 API 调用方法,支持路径参数、查询参数和请求体。
参数:
wrapper: 包装函数,用于处理实际的请求逻辑
返回: 包含 GET、POST、PUT、DELETE、PATCH 等方法的对象,每个方法都支持链式调用
示例:
const API = createShortApi(
async (url, method, params, query, body, options) => {
// 处理请求逻辑
const { url: _url } = parseUrl(url, { params, query });
const response = await fetch(_url, { ...options, method, body });
return response.json();
}
);
// 使用示例
const getUser = API.GET("/api/users/:id").Ok();
const userData = await getUser({ id: 1 });
// 使用 Params 进行类型安全定义
const getUserById = API.GET("/api/users/:id").Params<{ id: number }>().Ok<{ id: number; name: string }>();
const userData = await getUserById({ id: 1 });createShortMethods
创建快捷方法的工厂函数。
参数:
methods: HTTP 方法数组wrapper: 包装函数,接收方法名,返回处理请求的函数
返回: 包含指定方法的快捷调用对象
示例:
const methods = createShortMethods(["get", "post"] as const, (method) => {
return (url, options) => fetch(url, { ...options, method });
});
// 使用: methods.get('/api/users')parseOptions
解析 URL 选项。
参数:
urlOptions: 包含 url、options、baseURL 和 baseOptions 的对象
返回: 包含解析后的 url、options、is_body_json 和 abortController 的对象
示例:
const parsed = parseOptions({
url: "/api/users/:id",
options: { params: { id: "123" } },
baseURL: "https://api.example.com",
});
// 返回: { url: 'https://api.example.com/api/users/123', options: {...}, is_body_json: false, abortController: AbortController }mergeHeaders
合并多个 Headers 对象。
参数:
headersList: 要合并的 Headers 对象列表
返回: 合并后的 Headers 对象,后面的会覆盖前面的同名 header
示例:
const headers1: HeadersInit = { "Content-Type": "application/json" };
const headers2: HeadersInit = { Authorization: "Bearer token" };
const mergedHeaders = mergeHeaders(headers1, headers2);mergeSignals
合并多个 AbortSignal 信号。
参数:
signals: 要合并的 AbortSignal 数组timeout: 可选的超时时间(毫秒)
返回: 合并后的 AbortSignal,任意一个信号终止都会触发终止
示例:
const controller1 = new AbortController();
const controller2 = new AbortController();
const mergedSignal = mergeSignals(
[controller1.signal, controller2.signal],
5000
);parseUrl
合并 URL 及其相关参数。 处理 baseURL、路径参数和查询参数,生成完整 URL。
参数:
url: 原始 URLconfig: 配置对象,包含查询参数、路径参数和基础 URL
返回: 包含 url(完整 URL 字符串)、path(不含查询参数的 URL)和 query(URLSearchParams 实例)的对象
示例:
const { url } = parseUrl("/api/users/:id", {
params: { id: "123" },
query: { filter: "active" },
baseURL: "https://api.example.com",
});
// url: 'https://api.example.com/api/users/123?filter=active'parseQuery2Arr
将查询参数解析为 [key, value][] 数组。
支持多种输入格式:string、URLSearchParams、Record 和元组数组。
参数:
query: 任意支持的查询参数格式(string、URLSearchParams、Record、元组数组)
返回: [key, value] 键值对数组
示例:
parseQuery2Arr("page=1&size=10");
// 返回: [["page", "1"], ["size", "10"]]
parseQuery2Arr({ id: 123, status: "active" });
// 返回: [["id", "123"], ["status", "active"]]
parseQuery2Arr(new URLSearchParams("page=1"));
// 返回: [["page", "1"]]mergeOptions
合并多个选项对象。 合并请求选项,包括 headers 和 signal 等特殊处理。
参数:
optionsList: 要合并的选项对象列表
返回: 合并后的选项对象
示例:
const defaultOptions = {
timeout: 5000,
headers: { "Content-Type": "application/json" },
};
const requestOptions = {
method: "POST",
body: JSON.stringify({ name: "John" }),
};
const mergedOptions = mergeOptions(defaultOptions, requestOptions);isBodyJson
判断请求体是否为 JSON 对象。 检查 body 是否为普通对象,而不是 FormData、Blob 等特殊类型。
参数:
body: 请求体
返回: 如果是 JSON 对象返回 true,否则返回 false
示例:
isBodyJson({ name: "John" }); // true
isBodyJson(new FormData()); // false
isBodyJson("string"); // falsegenRequestKey
生成请求的唯一标识键(同步)。 根据请求的 URL、方法、headers、查询参数等生成唯一键值,用于缓存和请求共享。
注意: 这是同步版本。对于
Blob/FormData等需要异步处理的 body 类型,会回退为非确定性 key(基于 nonce)。使用genRequestKeyAsync可生成完全确定性的 key。 注意:genRequestKeyAsync使用的 SHA-256 摘要需要安全上下文(HTTPS)。在纯 HTTP 下crypto.subtle.digest不可用,genRequestKeyAsync会对Blob/ArrayBuffer/DataViewbody 回退为非确定性 key,避免错误命中缓存或请求共享。
参数:
req: 包含 url 和 options 的请求对象sortObjKeys?: 是否对对象键进行排序以获得稳定序列化
返回: 唯一标识字符串
示例:
const key = genRequestKey({
url: "/api/users",
options: { method: "GET", params: { id: 1 } },
});genRequestKeyAsync
生成请求的唯一标识键(异步)。
与 genRequestKey 功能相同,但支持异步处理 Blob/FormData body,生成完全确定性的 key。
参数:
req: 包含 url 和 options 的请求对象sortObjKeys?: 是否对对象键进行排序以获得稳定序列化
返回: 解析为唯一标识字符串的 Promise
示例:
const key = await genRequestKeyAsync({
url: "/api/users",
options: { method: "GET", params: { id: 1 } },
});createRequestStore
创建请求存储实例。 提供请求缓存、共享和竞态条件处理。
参数:
options: 配置选项maxCacheSize: 最大缓存大小(默认: 100000)
返回: 请求存储对象,包含以下方法:
entry(key): 获取特定请求键的条目dispose(): 销毁存储并清除定时器get(key): 根据键获取请求set(key, value): 根据键设置请求remove(key): 根据键移除请求getAll(): 获取所有请求removeAll(): 移除所有请求clearCache(): 清除所有缓存clearExpiredCache(): 清除过期缓存abortAll(): 中止所有请求
示例:
const store = createRequestStore({ maxCacheSize: 1000 });
// 在请求中使用 storedeepSort
深度排序对象键。 递归地对对象的键进行排序,用于生成稳定的对象序列化结果。
参数:
obj: 要排序的对象sortArr: 是否对数组进行排序(默认: false)
返回: 键排序后的对象
示例:
const obj = { b: 2, a: 1, c: { z: 3, y: 2 } };
const sorted = deepSort(obj);
// 返回: { a: 1, b: 2, c: { y: 2, z: 3 } }createSilentRefresh
创建静默刷新实例。 用于处理 token 过期时的静默刷新功能。
参数:
refresh_token_fn: 刷新 token 的函数
返回: 接收成功和失败回调的函数
示例:
const silentRefresh = createSilentRefresh(async () => {
// 刷新token逻辑
await refreshToken();
});
// 使用示例
silentRefresh(
() => console.log("刷新成功"),
() => console.log("刷新失败")
);soonFetch
一个轻量级的 fetch 包装器,支持缓存、共享和竞态条件处理。
参数:
config: 配置对象url: 请求 URLoptions: 请求选项 (SoonOptions)baseURL: 基础 URLbaseOptions: 基础请求选项store: 自定义请求存储sortRequestKey: 是否对请求键进行排序
返回: 解析为 Response 的 Promise
示例:
const response = await soonFetch({
url: "/api/users",
options: {
method: "GET",
query: { page: 1 },
share: true,
staleTime: 5000,
},
baseURL: "https://api.example.com",
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json() as User[];toFormData
将对象转换为 FormData。 用于将普通对象转换为 FormData 格式,支持文件和普通值。
参数:
body: 要转换的对象
返回: 转换后的 FormData 对象
示例:
const formData = toFormData({
name: "John",
avatar: fileBlob,
age: 30,
});progressDownload
带进度的下载。 用于下载文件并跟踪进度。
参数:
response: 响应对象onProgress: 进度回调函数
返回: 解析为 ArrayBuffer 的 Promise
示例:
const response = await fetch("/api/download");
const buffer = await progressDownload(response, (progress, downloaded, total) => {
console.log(`进度: ${progress}%, 已下载: ${downloaded}/${total}`);
});progressReadBody
带进度的读取响应体。 用于读取响应体并跟踪进度。
参数:
body: ReadableStreamonProgress: 进度回调函数total: 总大小(字节,默认: 0)
返回: 解析为 ArrayBuffer 的 Promise
示例:
const response = await fetch("/api/download");
const buffer = await progressReadBody(response.body, (progress, downloaded, total) => {
console.log(`进度: ${progress}%, 已下载: ${downloaded}/${total}`);
});requestWithStore
带存储支持的请求包装器。 用于处理带有缓存、共享和竞态条件处理的请求。
参数:
store: 请求存储实例requestFn: 请求函数requestKey: 请求键fetchAbort: AbortControlleroptions: 选项对象
返回: 解析为响应的 Promise
示例:
const store = createRequestStore();
const data = await requestWithStore(store, () => fetch(url, options), requestKey, abortController, {
share: true,
staleTime: 5000,
});English | 中文 | Installation
安装 Installation
npm install soon-fetchBest Practices / 最佳实践
Real-World Project Structure / 真实项目结构
Based on soon-admin-vue:
File Organization / 文件组织
src/api/
├── request.ts # Request wrapper with unified error handling
├── types.ts # Shared type definitions (e.g., PagedParams)
├── index.ts # Export all API modules
└── modules/
├── auth.ts # Authentication APIs
├── user.ts # User management APIs
├── role.ts # Role management APIs
├── dept.ts # Department management APIs
└── ... # Other domain-specific modulesStep 1: Create Request Wrapper (src/api/request.ts)
import { createSoon, soonFetch } from "soon-fetch";
import type { SoonOptions } from "soon-fetch";
type ReqOpts = SoonOptions & {
retry?: { max?: number; enable?: (result: any) => boolean };
toastErr?: boolean;
};
const request = async <T>(url: string, options?: ReqOpts): Promise<T> => {
const isGet = !options?.method || options?.method.toLowerCase() === "get";
try {
const response = await soonFetch({
url,
options,
baseURL: import.meta.env.VITE_API_BASE,
baseOptions: {
timeout: 20 * 1000,
headers: new Headers({ Authorization: localStorage.getItem("token") ?? "" }),
share: isGet,
staleTime: isGet ? 2 * 1000 : 0,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Auto-parse JSON
if (response.headers.get("content-type")?.includes("json")) {
const body = await response.json();
return body.data as T;
}
return response as unknown as T;
} catch (error: any) {
// Unified error handling
if (error.name === "TimeoutError") {
ElMessage.error("Request timeout");
} else if (error.name !== "AbortError" && options?.toastErr !== false) {
ElMessage.error(error.message);
}
throw error;
}
};
export const soon = createSoon<ReqOpts>(request);Key Points:
- ✅ Centralized error handling: All HTTP errors, timeouts, and business errors handled in one place
- ✅ Auto Token refresh: Handle 401 errors and retry automatically
- ✅ Smart caching: GET requests cached by default, write operations not cached
- ✅ No repetitive try-catch: Components call APIs directly without error handling boilerplate
Step 2: Define Typed APIs (src/api/modules/user.ts)
import { downloadBlob, getHeaderFilename } from "soon-utils";
import type { PagedParams } from "../types";
import type { Dept } from "./dept";
import type { Role } from "./role";
import { soon } from "../request";
// Type definitions
export type User = {
id: number;
username: string;
email: string | null;
phone: string | null;
name: string | null;
avatar: string | null;
roleId: number | undefined;
deptId: number | undefined;
status: number;
};
export type UserInfo = User & {
createTime: Date;
updateTime: Date | null;
dept?: Pick<Dept, "id" | "name">;
role?: Pick<Role, "id" | "name">;
};
type ListQueryUser = PagedParams & {
keyword?: string;
timeRange?: [string, string];
};
// CRUD APIs - simple and type-safe
export const list_user = soon.GET("/user").Query<ListQueryUser>().Ok<{ list: UserInfo[] }>();
export const add_user = soon.POST("/user").Body<User>().Ok();
export const update_user = soon.PUT("/user/:id").Body<User>().Ok();
export const del_user = soon.DELETE("/user/:id").Ok();
export const detail_user = soon.GET("/user/:id").Ok<UserInfo>();
// File download - returns Response for custom handling
export const download_user_table = async (query: ListQueryUser) => {
return soon.get<Response>("/user/export", { query }).then(async (res) => {
const body = await res.blob();
const filename = getHeaderFilename(res.headers) ?? "user.xlsx";
downloadBlob(body, filename);
});
};
// Captcha example
export const getCaptcha = soon.GET("/captcha").Ok<{ id: number; img: string }>();Key Points:
- ✅ Domain-based modules: Each module handles one business domain (user, role, dept, etc.)
- ✅ Type safety: Use
.Params<>(),.Body<>(),.Ok<>()for full type inference - ✅ Shared types: Import cross-domain types from sibling modules
- ✅ Special operations: Complex logic (file download) wrapped in async functions
- ✅ Naming convention: Use snake_case for API exports (e.g.,
list_user,add_user)
Step 3: Centralized Exports (src/api/index.ts)
export * from "./modules/auth";
export * from "./modules/user";
export * from "./modules/role";
// ... other modulesUsage in components:
import { list_user, add_user, del_user } from "@/api";Step 4: Use in Components - Clean and Simple!
Vue 3 Example:
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { list_user, del_user } from "@/api";
import type { UserInfo } from "@/api";
const users = ref<UserInfo[]>([]);
const loading = ref(false);
const fetchUsers = async () => {
loading.value = true;
try {
const { list } = await list_user({ page: 1, pageSize: 10 });
users.value = list;
} finally {
loading.value = false;
}
};
const handleDelete = async (id: number) => {
await del_user({ id });
await fetchUsers();
};
onMounted(fetchUsers);
</script>React Example with Race Condition Handling:
import { useEffect, useRef, useState } from "react";
import { list_user } from "@/api";
import type { UserInfo } from "@/api";
export default function UserList() {
const [users, setUsers] = useState<UserInfo[]>([]);
const [loading, setLoading] = useState(false);
const abortRef = useRef<[AbortController] | []>([]);
useEffect(() => {
setLoading(true);
list_user({ page: 1, pageSize: 10 }, { aborts: abortRef.current })
.then(({ list }) => setUsers(list))
.catch((err) => {
if (err.name !== "AbortError") console.error(err);
})
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(u => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}Key Points:
- ✅ No repetitive error handling: Errors already handled in request wrapper
- ✅ Race condition control: Use
useRefto manageabortsparameter - ✅ Clean code: Focus on business logic, not network error boilerplate
- ✅ Type safety: Full TypeScript support with autocomplete and type checking
Summary / 总结
Architecture Benefits:
- Separation of Concerns: Network layer (request.ts) vs Business layer (modules/) vs UI layer (components)
- DRY Principle: Error handling written once, used everywhere
- Type Safety: End-to-end type inference from API definition to component usage
- Maintainability: Domain-based modules make it easy to find and modify APIs
- Scalability: Easy to add new domains by creating new module files
Core Philosophy:
- 🎯 Centralize common logic in request wrapper (errors, retries, caching)
- 🎯 Keep API definitions simple with chainable type-safe methods
- 🎯 Components focus on UI without repetitive error handling code
- 🎯 Handle edge cases only when needed (race conditions, special business logic)
