Skip to content

Fetch based HTTP client with similar API to axios for browser and Node.js / 类似 Axios 语法的原生 fetch API 请求封装库

License

Notifications You must be signed in to change notification settings

dragon-fish/fexios

Repository files navigation

Fexios

Fexios Logo

npm version npm downloads Unit test FOSSA Status

Fetch based HTTP client with similar API to axios for browser and Node.js

fetch + axios = fexios (Just a joke)

简体中文 | English

Features

  • 🤯 Native fetch API (supports the Promise API)
  • 🤫 Method shortcuts (fexios.post())
  • 🔗 Hooks (intercept request and response)
  • 😏 Automatic transform request and response data
  • 😏 Automatic transforms for JSON data
  • 🤩 Instances with custom defaults
  • 🫡 Instance extendable
  • 😍 Fricking tiny size: ~6kb (gzipped)

Installation

Using package manager

# Node Package Manager
npm install fexios
# Why not pnpm
pnpm add fexios
# Or yarn?
yarn add fexios

Then import the library and enjoy:

// 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
import('https://unpkg.com/fexios?module').then(({ createFexios }) => {
  const fexios = createFexios(/* options */)
})
  • UMD bundle
<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.

new Fexios(configs: Partial<FexiosConfigs>)

FexiosConfigs
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' | 'text' | 'form' | 'blob' | 'arrayBuffer'
  fetch?: FetchLike
}
Defaults
const DEFAULT_CONFIGS = {
  baseURL: '',
  timeout: 0,
  credentials: undefined,
  headers: {},
  query: {},
  responseType: undefined,
  shouldThrow(response) {
    return !response.ok
  },
  fetch: globalThis.fetch,
}

Fexios#request(config: FexiosRequestOptions)

fexios.request<T>(config): Promise<FexiosResponse<T>>

FexiosRequestOptions
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
}

returns {FexiosFinalContext}

export type FexiosFinalContext<T = any> = {
  request: {
    url: string
    method?: string
    headers: Headers | Record<string, any>
    query: Record<string, any> | URLSearchParams
    body?: any
    rawRequest: Request
    // ... other request configs ...
  }
  runtime: {
    abortController?: AbortController
    onProgress?: (progress: number, buffer?: Uint8Array) => void
    customEnv?: any
  }
  /** Parsed response wrapper */
  response: IFexiosResponse<T>
  /** Unread original Response */
  rawResponse: Response
  /** Shortcut getters */
  readonly headers: Headers
  readonly data: T
  readonly responseType: 'json' | 'text' | 'form' | 'blob' | 'arrayBuffer'
  readonly url: string
  readonly rawRequest: Request
}
export interface IFexiosResponse<T = any> {
  ok: boolean
  status: number
  statusText: string
  headers: Headers
  rawResponse: Response
  data: T
  responseType: 'json' | 'text' | 'form' | 'blob' | 'arrayBuffer'
}

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.request.url is resolved against defaults.baseURL.
    • Search params from defaults.baseURL are merged into ctx.request.url.
    • Priority: ctx.request.url search params > defaults.baseURL search params.
  • Query: defaults.query is merged into ctx.request.query.
    • Priority: ctx.request.query > defaults.query.
  • Headers: defaults.headers is merged into ctx.request.headers.
    • Priority: ctx.request.headers > defaults.headers.

2. Finalize Request (Before beforeActualFetch)

This happens when constructing the native Request object.

  • Query: ctx.request.query is merged into the final URL's search params.
    • Priority: ctx.request.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.request.url in hooks (e.g. beforeRequest) will NOT be parsed into ctx.request.query. They are treated as separate entities until the final merge.
  • If you replace ctx.request.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.request.query.

Hooks

You can modify context in hooks' callback then return it as a brand new context™.

Return false to abort request immediately.

export type FexiosHook<C = unknown> = (
  context: C
) => AwaitAble<C | void | false | Response>
export interface FexiosContext<T = any> {
  request: {
    url: string // may change after beforeInit
    query: Record<string, any> | URLSearchParams
    headers: Headers | Record<string, any>
    body?: any
    rawRequest?: Request // available in beforeActualFetch
  }
  runtime: {
    abortController?: AbortController
    onProgress?: (progress: number, buffer?: Uint8Array) => void
    customEnv?: any
  }
  rawResponse?: Response // available in afterRawResponse
  response?: IFexiosResponse<T> // available in afterResponse
  // NOTE: legacy aliases like ctx.url / ctx.query may exist but are deprecated.
}
Hooks example
const fexios = new Fexios()

fexios.on('beforeRequest', async (ctx) => {
  ;(ctx.request.headers as any).authorization = localStorage.getItem('token')
  const q = ctx.request.query as any
  if (q.foo === 'bar') {
    return false
  } else {
    q.foo = 'baz'
    return ctx
  }
  return ctx
})

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.request.rawRequest: {Request} now available.

The Request instance has been generated.

At this time, you cannot modify ctx.request.url, ctx.request.query, ctx.request.headers or ctx.request.body (etc.) anymore, unless you pass a brand new Request to replace ctx.request.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:

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.

// 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) => {})

Plugin

See the plugin docs index: docs/plugins/README.md

Official plugins:


License

MIT License

Copyright (c) 2023 机智的小鱼君 (A.K.A. Dragon-Fish)

Fexios' logo is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0).

Copyright (c) 2025 dragon-fish

This license applies ONLY to the logo files in docs/logos/ directory.

FOSSA Status

About

Fetch based HTTP client with similar API to axios for browser and Node.js / 类似 Axios 语法的原生 fetch API 请求封装库

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •