Fennch: like axios, but based on fetch

There is a thing about me that I can't express better than Marijn Haverbeke in his blog post about ProseMirror:

Sometimes I lie awake at night, feverishly searching for new ways to load myself down with more poorly-paying responsibilities. And then it comes to me: I should start another open-source project!

(It's completely off-topic, but if you interested in open source tools for web, check out his blog post and ProseMirror framework in general. This guy is doing a fantastic job.)

Well, like with Marijn, it's not literally what happened. Just one day on my work I have to use new (not really by now…) fetch Web API. I need to intercept requests in ServiceWorker and process data loaded from the server before passing it to the main application.

Before then I just use axios like almost everyone else, and all was cool and simple.

Here where things start to get complicated.

fetch vs. xhr

New fetch object introduced in Web-browsers around 2014 aimed to replace XMLHttpRequest (or just xhr) with cool features like Promise-based API and ability to intercept requests in ServiceWorker.

Since then fetch came to all modern browsers, numerous articles about how cool it is were written but adoption of this API does not look quite broad.

For a while fetch didn't support request abortion (now implemented with AbortController) and ability to intercept requests in ServiceWorker looks not attractive enough (since ServiceWorker mostly used only to cache application assets for offline use and add annoying notifications).

When it comes to Promise, there are a lot of solutions that wrap up XMLHttpRequest and exposes Promise-based API (like Axios).
So most developers just don't bother.

Mostly, my problem with fetch is it is just raw low-level API, and if you need nice things like request and response interceptors, common headers for all requests, a request abortion, and other advanced features you find yourself writing a lot of not really elegant code. So I started to look a universal solution. This API is around for several years, so in a world where everything has JavaScript implementation there must be several libraries that make fetch easy and pleasant to use, right?

Ha-ha.

Axios, superagent, request

...are all xhr-based. So no request interception in ServiceWorker. Miss.

I found a library called Frisbee. It looks promising at first, and I thought I could implement some missing features (like request abortion and timeout support) and contribute there. But in the process, I found myself rewrote a lot of code in this library. It was too much for just a PR since I changed fundamental things in Frisbee design. It was effectively a new library, so… Time for the new library: Fennch

API design and fundamentals

When I was writing a new library I have two goals:

  1. Create nice Axios-like API for request interception, abortion, and timeout
  2. Avoid new abstractions and keep native fetch behavior as possible

First part is pretty straightforward.

In Fennch, you can create a base object like in Axios, pass there some defaults like base server URL and headers.

const api = Fennch({
    baseUri: "http://awesome.app/api"
})

After that, you can use methods like get, post etc. to make requests and overwrite defaults on a per-request basis if you need to.

const result = await api.get("/awesome-data", {
    params: { // params for `get` is serializing to query string
        awesome: "really",
        params: "cool"
    }
});

Second is a little bit more tricky to implement.

Internally, fetch use Request, Response, Headers and Body objects exposed by Web API. If you call fetch with just URL, Request is created implicitly, but you can also create it manually and pass to fetch.
Call result would be Response object.

Since one of the Fennch goals is to keep usage logic close to native, it also uses special Request and Response objects, but here's the trick. Native objects have low-level API and not pleasant to use. Headers are special Headers object, and though you can pass headers values in the fetch config or in constructor of Request object, if you want to modify headers in existing Request you need to use special Headers methods, or they even can't be modified at all if Request was already used and headers becomes immutable.

Same thing with Response. You can't just access the Response body by key. You need to serialize it using .json() or others, it depends on content type set up in request configuration. First, these methods return Promise, and it forces you to make your code explicitly asynchronous. Second, once the request body is serialized, it becomes unavailable and cannot be serialized again. So you need to store serialization result somewhere outside of Request.

Fennch deals with all these caveats using Proxy. It wraps native objects in special fRequest and fResponse proxy objects, and they handle all operation need internally and gives you simple, intuitive API, like this:

import Fennch, { createFRequect } from "fennch";

const request = createFRequect("my/super/api", {
    params: { search: "my search string" }
    // here goes configuaration, identical to native `Request`
});

// Here we can just set headers like in normal Object
req.headers["Content-Type"] = "application/json"
req.headers.Authorization = `Bearer ${JWTToken}`

Fennch().req(request).then(response => {
  console.log(res.body) // Body already serialized and available
  console.log(response.requests.params.search) // yeah, you request is available too!
})

This approach make request modification trivial and used, for example, in Fennch interceptor mechanism.

Po(l|n)yfilling

Support of old browsers and Node can give a lot of headaches and, the thing is, looks like all polyfilling solutions have some caveats, so in Fennch you can pass optional fetch implementation as an argument.

const fennch = Fennch(options, myFetchPolyfill)

Or omit it to just use native browser fetch.

Abort and timeout

Request abortion in fetch was not available for a long time, but now it possible with AbortController. I will not describe it here, you can read about it yourself on MDN. And... same thing, it does not look elegant. It's raw, sharp and requires you to write a lot of boilerplate code in order to use it. Though all this not a bad thing for low-level API, some more universal high-level API would be nice.

See how Fennch deals with it:

const myRequest = Fennch().get('/hello'); // `.get` returns Promise and we store it in `myRequest` variable

// ...then some time pass, something happened, and we need to abort the request...

myRequest.abort(); // Thats it. That simple

Internally, Fennch use AbortablePromise, special wrapper for native Promise, that adds .abort() method.

Request timeout uses the same mechanism, and you can set up timeout globally:

const fennch = Fennch({
  timeout: 10000 // timeout in milliseconds
})

...or per-request basis:

fennch.get('/hello', { timeout: 5000 })

Interceptors

With Fennch you can intercept requests and responses and modify them before passing further to pipeline:

const fennch = Fennch({ /* config */ })
fennch.interceptor.register({
  request(request) {
    // Here we can manipulate request
    return request // returns `fRequest` object to pass it further to pipeline
  },
  requestError(res) {
    console.trace('Request error: ', err)
    return Promise.reject(err); // Returns Promise (here — Promise rejection)
  },
  response(response) { // normal response interceptor
    // Here we can manipulate response
    return response; // returns `fResponse` object to pass it further to pipeline
  },
  responseError(err) { // response error interceptor
    console.trace('Response error: ', err)
    return Promise.reject(err); // Returns Promise (here — Promise rejection)
  }
})

Also, you can register several interceptors using fennch.interceptor.register.

Practical example: adding an authorization header with JWT token to all requests:

fennch.interceptor.register({
  request(req) {
    if (req.url !== '/auth/refresh' && req.url !== '/auth') {
      req.headers['Authorization'] = `Bearer ${JWT_TOKEN}`
    }
    return req
  }
})

Platforms support

Fennch relies hugely on Proxy that can't be polyfilled, so no support for IE or any runtime that doesn't implement them.
You can find support list on Can I Use?.

Usual things

Fennch is not stable and production-ready yet, it needs proper auto-testing, but we use it heavily in a big project on my day job, and it works fine.
You can join to development and help make good experience with modern web technologies a reality 😉.

Fennch source | Docs in README | Bugs and feature requestsNPMMIT licensed