ES-Fetch-API
| 中文 | English |
Very powerful and extensible HTTP client for modern JavaScript runtimes with Fetch API support.
AI Agent Support
This project provides built-in skills for AI agents (like Trae, Cursor, etc.) to understand and write code using es-fetch-api.
Installation
Run the following command in your project root to install the AI skills to .ai/skills and .agent/skills:
npx add-skill lchrennew/es-fetch-api
Why use ES-Fetch-API?
Still using axios? ES-Fetch-API keeps your code lighter and easier to read.
i. It is extremely lightweight and built on the native Fetch API
Compared to axios at roughly 400kB, es-fetch-api is only about 6kB of source code because it is designed for
modern JavaScript runtimes that already support the native Fetch API.
References:
ii. Enables maximized readability, extensibility, maintainability and minimized complexity.
1. The simplest example
Expected request:
GET http://yourdomain.com/api/v1/user?id=12345
Using axios:
import axios from 'axios'
// no need to explain that 'http://yourdomain.com/api/v1' is the base URL
const apiV1 = axios.create({ baseURL: 'http://yourdomain.com/api/v1' })
// no need to explain that `/user` is the URL
const getUser = id => apiV1.get({ url: `/user`, params: { id } })
const response = await getUser(12345)
Using es-fetch-api, great readability:
import { getApi, query } from "es-fetch-api";
// without mincing words
const apiV1 = getApi('http://yourdomain.com/api/v1')
const getUser = id => apiV1(`user`, query({ id }))
const response = await getUser(12345)
2. More complicated example (using built-in middlewares)
Expected request:
POST http://yourdomain.com/api/v1/user/
Content-Type: application/json
{"firstName":"Fred","lastName":"Flintstone"}
Using axios:
import axios from 'axios'
const apiV1 = axios.create({ baseURL: 'http://yourdomain.com/api/v1' })
// which format is used to post data?
const createUser = user => apiV1.post(`/user`, user)
const response = await createUser({
firstName: 'Chun',
lastName: 'Li'
})
Using es-fetch-api, better readability:
import { getApi, json, POST } from "es-fetch-api";
const apiV1 = getApi('http://yourdomain.com/api/v1')
// read exactly what you see
const createUser = user => apiV1(`user`, POST, json(user))
const response = await createUser({
firstName: 'Chun',
lastName: 'Li'
})
3. Create custom middleware to extend your code while keeping better readability.
Expected request:
POST http://yourdomain.com/api/v1/user/
Content-Type: application/json
Authorization: Token ********
X-Timestamp: ##########
{"firstName":"Fred","lastName":"Flintstone"}
Using axios:
import axios from 'axios'
import { getToken } from 'token-helper'
// easy to read? It is hard to tell that these functions return headers.
const useToken = async () => ({ 'Authorization': `Token ${await getToken()}` })
const useTimestamp = async () => ({ 'X-Timestamp': Date.now() })
const apiV1 = axios.create({ baseURL: 'http://yourdomain.com/api/v1' })
// easy to read? Maybe, but it is still too long-winded to maintain.
const createUser = async user => await apiV1.post({
url: `/user`,
data: user,
headers: { ...await useToken(), ...await useTimestamp() }
})
const response = await createUser({
firstName: 'Chun',
lastName: 'Li'
})
Using es-fetch-api, better readability, better maintainability:
import { getApi, json, POST } from "es-fetch-api";
import { getToken } from 'token-helper'
// read what you see
const useToken = async (ctx, next) => {
ctx.header('Authorization', `Token ${await getToken()}`)
return next()
}
const useTimestamp = (ctx, next) => {
ctx.header('X-Timestamp', Date.now())
return next()
}
const apiV1 = getApi('http://yourdomain.com/api/v1')
// read exactly what you see
const createUser = user => apiV1(`user`, POST, json(user), useToken, useTimestamp)
const response = await createUser({
firstName: 'Chun',
lastName: 'Li'
})
4. Use custom middlewares for every invocation
Using axios:
import axios from 'axios'
import { getToken } from 'token-helper'
const useToken = async () => ({ 'Authorization': `Token ${await getToken()}` })
const useTimestamp = () => ({ 'X-Timestamp': Date.now() })
// the headers are static here, especially X-Timestamp. Easy to maintain? No.
const apiV1 = axios.create({
baseURL: 'http://yourdomain.com/api/v1',
headers: { ...await useToken(), ...useTimestamp() }
})
const createUser = user => apiV1.post({ url: `/user`, data: user, })
const getUser = id => apiV1.get({ url: `/user`, params: { id } })
Using es-fetch-api, better readability, better maintainability:
import { getApi, json, POST } from "es-fetch-api";
import { getToken } from 'token-helper'
const useToken = async (ctx, next) => {
ctx.header('Authorization', `Token ${await getToken()}`)
return next()
}
const useTimestamp = (ctx, next) => {
ctx.header('X-Timestamp', Date.now())
return next()
}
// Just append the middlewares, so easy.
const apiV1 = (...args) => getApi('http://yourdomain.com/api/v1')(...args, useToken, useTimestamp)
const createUser = async user => await apiV1(`user`, POST, json(user))
5. Process responses
For instance with the getUser function.
When the user exists, the response should be:
Status: 200 OK
Content-Type: application/json
Body: {ok: true, data: {"firstName": "Chun", "lastName": "Li"}}
When the user does not exist, the response should be:
Status: 404 NotFound
Content-Type: application/json
Body: {ok: false, message: 'User doesn't exist.'}
Using axios:
import axios from 'axios'
const apiV1 = axios.create({ baseURL: 'http://yourdomain.com/api/v1' })
const getUser = async id => {
try {
const response = await apiV1.get({ url: `/user`, params: { id } })
console.log(response.status, response.statusText)
// response.data and error.response.data are easy to mix up
const { data } = response.data
return data
} catch (error) {
// which error object should I use here?
console.log(error.response.data.message ?? error.message)
}
}
Using es-fetch-api, great readability:
import { getApi, query } from "es-fetch-api";
const apiV1 = getApi('http://yourdomain.com/api/v1')
const getUser = async id => {
try {
const response = await apiV1(`user`, query({ id }))
console.log(response.status, response.statusText)
const { ok, data, message } = await response.json() // read what you see
if (!ok) throw { message } // throw the error as you will
return data
} catch (error) {
console.log(error.message)
}
}
6. Process responses in a unified way
Using axios:
import axios from 'axios'
// can you understand it?
// There seems no way to process errors in a unified way?
const apiV1 = axios.create({ baseURL: 'http://yourdomain.com/api/v1' })
const getOne = async config => {
try {
const response = await apiV1(config)
console.log(response.status, response.statusText)
const { ok, data, message } = response.data
return data
} catch (error) {
console.log(error.response.data.message ?? error.message)
}
}
Using es-fetch-api, great readability:
import { getApi, query } from "es-fetch-api";
const apiV1 = getApi('http://yourdomain.com/api/v1')
const getOne = async (...args) => {
try {
const response = await apiV1(...args, useToken) // you can append custom middlewares here
console.log(response.status, response.statusText)
const { ok, data, message } = await response.json() // read what you see
if (!ok) throw { message } // throw the error as you will
return data
} catch (error) {
console.log(error.message ?? error)
}
}
// getOne can process every response in a unified way. You can also build helpers such as getList
// read what you see
const getUser = id => getOne(`user`, query({ id }))
One word reason
In es-fetch-api, every API invocation is a middleware chain. That makes the whole flow extensible without adding
extra complexity, whether you want to handle requests and responses case by case or in a unified way.
Built-in middlewares
Built-in middlewares are now exported from both es-fetch-api and es-fetch-api/middlewares, and the examples below use the package root as the default import entry.
method middleware
This middleware sets the HTTP method. It accepts a string parameter for the method name. If an unsupported method name is used, an exception will be thrown.
import { getApi, method } from "es-fetch-api";
const api = getApi('http://mydomain.com/api')
const response = api('/', method('DELETE'))
method aliases
GET, POST, PUT, PATCH and DELETE, these are shorthands for each corresponding method.
import { getApi, DELETE } from "es-fetch-api";
const api = getApi('http://mydomain.com/api')
const response = api('/', DELETE)
json middleware
This middleware declares that the HTTP request body is a JSON object.
It accepts an Object parameter as the request body.
When you use this middleware, the Content-Type: application/json header will be set automatically.
import { getApi, json, POST } from "es-fetch-api";
const api = getApi('http://mydomain.com/api')
const response = api('/', POST, json({ hello, world }))
query middleware
This middleware declares the query string parameters of the request URL.
It accepts two parameters.
- an
Object, whose keys are query parameter names and whose values are the corresponding query parameter values. If a value is an array with more than one element, it becomes a multi-value parameter. - a
Boolean, used to indicate whether each query parameter value should be appended to existing values. By default, it isfalse.
import { getApi, query } from "es-fetch-api";
const api = getApi('http://mydomain.com/api?hello=1')
api(query({ hello: 'world' })) // http://mydomain.com/api?hello=world
api(query({ hello: 'world' }, true)) // http://mydomain.com/api?hello=1&hello=world
api(query({ hello: [ 'Bing', 'Dwen', 'Dwen' ], world: '2022' })) // http://mydomain.com/api?hello=Bing&hello=Dwen&hello=Dwen&world=2022
form middleware
This middleware declares that the HTTP request body is form data.
It accepts an Object parameter as the form data.
When you use this middleware, the Content-Type: application/x-www-form-urlencoded header will be set automatically.
import { getApi, form, POST } from "es-fetch-api";
const api = getApi('http://mydomain.com/api')
api(POST, form({ hello: 'world' })) // hello=world
file middleware
This middleware declares that the HTTP request uploads files.
It accepts three parameters:
- the name of the
FormDatafield that contains the file - a
Fileobject - an optional filename, which defaults to the original filename
abortable middleware
This middleware injects AbortController::signal into fetch, so that you can abort the request as you wish.
You can use it to implement manual abort, timeout abort and so on.
When the AbortController::abort() is invoked, an exception will be thrown.
import { getApi, abortable } from "es-fetch-api";
const api = getApi('http://mydomain.com/api')
const controller = new AbortController()
setTimeout(() => controller.abort(), 1000)
api(abortable(controller))
Middleware development
Each middleware follows the same signature (Function and AsyncFunction are both fine) and finally returns next().
const example = (ctx, next) => {
// TODO: your logic
return next()
}
or
const example = async (ctx, next) => {
// TODO: your logic
return next()
}
More about the ctx
ctx is almost the same as a Request, except that it exposes a helper
method for setting request headers. See the useToken middleware example in this document.