fexios
Version:
Fetch based HTTP client with similar API to axios for browser and Node.js
373 lines (274 loc) • 9.42 kB
Markdown
<div align="center">
# Fexios
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fdragon-fish%2Ffexios?ref=badge_shield)
Fetch based HTTP client with similar API to axios for browser and Node.js
~~fetch + axios = fexios~~ (Just a joke)
</div>
[简体中文](README.zh_CN.md) | [English](README.md)
## Features
- [x] 🤯 Native fetch API (supports the Promise API)
- [x] 🤫 Method shortcuts (`fexios.post()`)
- [x] 🔗 Hooks (intercept request and response)
- [x] 😏 Automatic transform request and response data
- [x] 😏 Automatic transforms for JSON data
- [x] 🤩 Instances with custom defaults
- [x] 🫡 Instance extendable
- [x] 😍 Fricking tiny size: `index.umd.cjs 8.51 kB │ gzip: 3.48 kB │ map: 31.96 kB`
## Installation
**Using package manager**
```sh
# Node Package Manager
npm install fexios
# Why not pnpm
pnpm add fexios
# Or yarn?
yarn add fexios
```
Then import the library and enjoy:
```ts
// Using directly
import fexios from 'fexios'
fexios.get('https://zh.moegirl.org.cn/api.php')
// Yes, it's callable! Just like axios
fexios({
url: 'https://zh.moegirl.org.cn/api.php',
method: 'GET',
})
fexios('https://zh.moegirl.org.cn/api.php', {
method: 'POST',
body: { foo: 'bar' },
})
// Customize instance
import { createFexios, Fexios } from 'fexios'
const fexios = createFexios(/* options */)
const fexios = new Fexios(/* options */)
const fexios = Fexios.create(/* options */)
// Custom instance is also callable
fexios('https://zh.moegirl.org.cn/api.php', {
method: 'POST',
body: { foo: 'bar' },
})
```
**Use directly in the browser**
- ES Module
```ts
import('https://unpkg.com/fexios?module').then(({ createFexios }) => {
const fexios = createFexios(/* options */)
})
```
- UMD bundle
```html
<script src="https://unpkg.com/fexios/lib/index.js"></script>
<script>
// Using directly
const { fexios } = window.Fexios
fexios.get('https://zh.moegirl.org.cn/api.php')
// With options
const { createFexios } = window.Fexios
const fexios = createFexios(/* options */)
</script>
```
## Compatibility
Refer: https://developer.mozilla.org/docs/Web/API/Fetch_API
| Chrome | Edge | Firefox | Opera | Safari | Node.js |
| ------ | ---- | ------- | ----- | --------------- | ---------------------- |
| 42 | 14 | 39 | 29 | 10.1 (iOS 10.3) | ^16.15.0 \|\| >=18.0.0 |
\* Abort signal requires higher version.
## Usage
You can find some sample code snippets [here](test/).
### new Fexios(configs: Partial\<FexiosConfigs>)
<details>
<summary>FexiosConfigs</summary>
```ts
export interface FexiosConfigs {
baseURL: string
timeout: number
/**
* In context, query value can be:
* - `null` - to remove the item
* - `undefined` - to keep the item as is
*/
query: Record<string, any> | URLSearchParams
headers: Record<string, string | string[]> | Headers
credentials?: RequestInit['credentials']
cache?: RequestInit['cache']
mode?: RequestInit['mode']
responseType?: 'json' | 'blob' | 'text' | 'stream' | 'arrayBuffer'
fetch?: FetchLike
}
```
</details>
<details>
<summary>Defaults</summary>
```ts
const DEFAULT_CONFIGS = {
baseURL: '',
credentials: 'same-origin',
headers: {
'content-type': 'application/json; charset=UTF-8',
},
query: {},
responseType: 'json',
fetch: globalThis.fetch,
}
```
</details>
### Fexios#request(config: FexiosRequestOptions)
`fexios.request<T>(config): Promise<FexiosResponse<T>>`
<details>
<summary>FexiosRequestOptions</summary>
```ts
export interface FexiosRequestOptions extends Omit<FexiosConfigs, 'headers'> {
url?: string | URL
method?: FexiosMethods
/**
* In context, header value can be:
* - `null` - to remove the header
* - `undefined` - to keep the header as is
*/
headers: Record<string, string | string[] | null | undefined> | Headers
body?: Record<string, any> | string | FormData | URLSearchParams
abortController?: AbortController
onProgress?: (progress: number, buffer?: Uint8Array) => void
}
```
</details>
**returns {FexiosFinalContext}**
```ts
export type FexiosFinalContext<T = any> = Omit<
FexiosContext<T>,
'rawResponse' | 'response' | 'data' | 'headers'
> & {
rawResponse: Response
response: IFexiosResponse<T>
headers: Headers
data: T
}
export interface IFexiosResponse<T = any> {
ok: boolean
status: number
statusText: string
headers: Headers
rawResponse: Response
data: T
}
```
And common request methods aliases:
- fexios.get(url[, config])
- fexios.delete(url[, config])
- fexios.head(url[, config])
- fexios.options(url[, config])
- fexios.post(url[, data[, config]])
- fexios.put(url[, data[, config]])
- fexios.patch(url[, data[, config]])
## Automatic Merge for Queries/Headers
The url/query/headers parameters you pass in various places will be automatically merged to build the complete request.
### Merge Strategy
Fexios uses a simplified 2-stage merge strategy:
#### 1. Apply Defaults (After `beforeInit`)
This happens only ONCE, immediately after the `beforeInit` hook.
- **URL**: `ctx.url` is resolved against `defaults.baseURL`.
- Search params from `defaults.baseURL` are merged into `ctx.url`.
- Priority: `ctx.url` search params > `defaults.baseURL` search params.
- **Query**: `defaults.query` is merged into `ctx.query`.
- Priority: `ctx.query` > `defaults.query`.
- **Headers**: `defaults.headers` is merged into `ctx.headers`.
- Priority: `ctx.headers` > `defaults.headers`.
#### 2. Finalize Request (Before `beforeActualFetch`)
This happens when constructing the native `Request` object.
- **Query**: `ctx.query` is merged into the final URL's search params.
- Priority: `ctx.query` > URL search params (from step 1 or modified by hooks).
- **Headers**: Final headers are built.
### Merge Rules
- **undefined**: Keeps the value from the lower layer (or no change).
- **null**: Removes the key from the result.
- **value**: Overwrites the lower layer.
### Note on Hooks
- Modifications to `ctx.url` in hooks (e.g. `beforeRequest`) will **NOT** be parsed into `ctx.query`. They are treated as separate entities until the final merge.
- If you replace `ctx.url` in a hook, you lose the original URL search params unless you manually preserve them.
- To modify query parameters reliably in hooks, prefer operating on `ctx.query`.
## Hooks
You can modify context in hooks' callback then return it as a brand new context™.
Return `false` to abort request immediately.
```ts
export type FexiosHook<C = unknown> = (
context: C
) => AwaitAble<C | void | false>
export interface FexiosContext<T = any> extends FexiosRequestOptions {
url: string // may changes after beforeInit
rawRequest?: Request // provide in beforeRequest
rawResponse?: Response // provide in afterRequest
response?: IFexiosResponse // provide in afterRequest
data?: T // provide in afterRequest
}
```
<details>
<summary>Hooks example</summary>
```ts
const fexios = new Fexios()
fexios.on('beforeRequest', async (ctx) => {
ctx.headers.authorization = localStorage.getItem('token')
if (ctx.query.foo === 'bar') {
return false
} else {
ctx.query.foo = 'baz'
return ctx
}
return ctx
})
```
</details>
### beforeInit
All context passed as is. You can do custom conversions here.
### beforeRequest
Pre-converted done.
### afterBodyTransformed
- `ctx.body`: `{string|URLSearchParams|FormData|Blob}` now available.
JSON body has been transformed to JSON string. `Content-Type` header has been set to body's type.
### beforeActualFetch
- `ctx.rawRequest`: `{Request}` now available.
The Request instance has been generated.
At this time, you cannot modify the `ctx.url`, `ctx.query`, `ctx.headers` or `ctx.body` (etc.) anymore. Unless you pass a brand new `Request` to replace `ctx.rawRequest`.
### afterResponse
Anything will be read-only at this time.
ctx is `FexiosFinalContext` now.
### Short-circuit Response
A hook callback can also return a `Response` at any time to short-circuit the request flow; Fexios will treat it as the final response and proceed to `afterResponse`:
```ts
fx.on('beforeActualFetch', () => {
return new Response(JSON.stringify({ ok: 1 }), {
status: 200,
headers: { 'content-type': 'application/json' },
})
})
```
### interceptors
Oh, this is mimicked from axios. Just sweet sugar.
<!-- prettier-ignore-start -->
```ts
// They are the same
fexios.on('beforeRequest', async (ctx) => {})
fexios.interceptors.request.use((ctx) => {})
// Bro, they're just the same
fexios.on('afterResponse', async (ctx) => {})
fexios.interceptors.response.use((ctx) => {})
```
<!-- prettier-ignore-end -->
## Plugin
```ts
import type { FexiosPlugin } from 'fexios'
const authPlugin: FexiosPlugin = (app) => {
app.on('beforeRequest', (ctx) => {
ctx.headers = { ...ctx.headers, Authorization: 'Bearer token' }
return ctx
})
return app // You can return app, or omit the return value
}
const fx = new Fexios().plugin(authPlugin)
```
---
## License
> MIT License
>
> Copyright (c) 2023 机智的小鱼君 (A.K.A. Dragon-Fish)
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fdragon-fish%2Ffexios?ref=badge_large)