Achieve Axios' Interceptor's Functionality with Ky

Contents

Background

axios is the most popular choice (based on my personal experience) when Vue/React developers need an HTTP client, and that’s also the case in a Vue 3 course that I have been following recently. However, I personally prefer the more platform-native (and hopefully more stable) Fetch API so I decided to go with my preference and use Fetch throughout the project. But if you’ve written plain Fetch code, you would know that it’s kinda verbose. Imagine writing plain XMLHttpRequest code without using axios. Luckily, we have ky to our rescue.

ky, just like axios, is an HTTP client library that we can use in the browser, key difference being it’s based on Fetch rather than XMLHttpRequest. There are also many other HTTP client libraries you can choose from the community and a comparison can be found at got ’s repo.

What are we trying to achieve?

Using ky should be pretty straightforward if you follow its documentation. However, in the course I follow they have some addition security measures: In order to access a private API endpoint the instructor provides us, we need to:

  • include our access code as a query string at the end of URL and
  • append our access code to the payload if we are submitting a form with FormData
  • include our access code in the payload if we are submitting JSON

Such global options can be configured in one axios/ky instance, which then gets exported and used all across your application. There is a well-known solution in axios: interceptors. Note that there are two types of interceptors, one for request and one for response. We only need to hook into the request interceptor here. The axios example would look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import axios from "axios";

const secretCode = "******";

axios.defaults.baseURL = "http://apis.somecourseurl.com/api/";

axios.interceptors.request.use((config) => {
  config.params = { ...config.params, secretCode };
  if (config.data instanceof FormData) {
    config.data.append("icode", "******");
  } else {
    config.data = { ...config.data, secretCode: "******" };
  }
  return config;
});

export axios

How are we going to do this in ky?

TL;DR;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import ky, { Options } from "ky";

const baseUrl = "http://apis.somecourseurl.com/api/";
const secretCode = "this-is-super-secret-code";
const secretCodeParam = `secretCode=${secretCode}`;

const addSecretCodeToPostBody = (request: Request, options: Options) => {
  if (request.method == "POST") {
    if (options.body instanceof FormData) {
      // INCORRECT:
      // const newBody = options.body;
      // newBody.append("secretCode", secretCode);
      // return new Request(request, {
      //   body: newBody,
      // });

      // UPDATED:
      request.headers.delete("content-type");
      options.body.append("icode", icode);
      return new Request(request, options as RequestInit);
    } else if (options.json instanceof Object) {
      return new Request(request, {
        body: JSON.stringify({ ...options.json, secretCode }),
      });
    }
  }
};

export const api = ky
  .create({ prefixUrl: baseUrl, searchParams: icodeParam })
  .extend({
    hooks: {
      beforeRequest: [addSecretCodeToPostBody],
    },
  });

export default api;

What did we do here? First, ky doesn’t have the concept of “interceptor”. Instead, it provides a set of hooks where we can tap into the lifecycle and make our changes to our requests and responses. More details can be found here . What we need to use is the beforeRequest hook.

Inside beforeRequest, the hook functions will receive two arguments: request of type Request and options, which is more or less of type RequestInit defined in TypeScript’s dom lib.

What we can do inside the hook function is described as such in ky’s doc:

The hook can return a Request to replace the outgoing request, or return a Response to completely avoid making an HTTP request.

Therefore, we are doing the following things with these two arguments:

  • Determine the HTTP method. Only modify the payload if it’s a POST request.
  • Grab the POST body from options.body
    • If the body is an instance of FormData, i.e. the request is a form submission with Content-Type of multipart/form-data, append the secretCode at the end.
    • If the payload is in options.json, we include our secretCode inside it.
  • Return a new Request based off of the original request, with a only the options.body modified:
    1
    2
    
    // basically a copy constructor
    return new Request(request, {body: <Your New Body>});
    

Update 2020-03-14:

The original proposed method was discovered to be problematic. The key thing here is for FormData to be correctly parsed, the request header Content-Type will need to include something called boundary (see the screenshot below). It’s used to separate different fields inside the FormData’s body.

Content-Type Header with Boundary

When we create a new request with return new Request(request, {body: <Your New Body>}), we are reusing the Content-Type header and thus the outdated boundary value while a new boundary value gets generated for the updated FormData instance. Therefore, the boundary in the header and the one in the request body is out of sync. (I don’t know how and when the boundary gets generated and I’d be super happy if you can teach me about it! 😄) No matter what your backend is, the server will not be able to parse the form payload. This is also why when you use Fetch API, you would not want to set the Content-Type header yourself if you are submitting a FormData payload.

Some Extra Tricks

With the hooks, we can achieve some cool nifty tricks that would be cumbersome otherwise. One example would be to have a automatic loading effect while the request is pending.

Let’s say you have a pair of actions defined in you Vuex store named showLoading and hideLoading that toggle a global state property isShowingLoadingScreen which then toggles the visibility of your global loading screen mask. You can then do something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// ... same code as before

const showLoading = () => {
  store.dispatch("showLoading");
};
const hideLoading = () => {
  store.dispatch("hideLoading");
};

// modify your ky instance configuration
export const api = ky
  .create({ prefixUrl: baseUrl, searchParams: icodeParam })
  .extend({
    hooks: {
      beforeRequest: [/*new->*/ showLoading, addIcodeToPostBody],
      afterResponse: [/*new->*/ hideLoading],
    },
  });

Here we also made use of the afterResponse hook.

NOTICE that we are putting the showLoading function at the beginning. Order matters! Or rather, the position of the hook function that returns a Request matters. As described in the doc:

An important consideration when returning a request or response from this hook is that any remaining beforeRequest hooks will be skipped, so you may want to only return them from the last hook.

Conclusion

There you have it! A nicely configured ky instance at your disposal. In this post we learned about how you can make use of the beforeRequest and afterResponse hooks to achieve similar functionalities as in axios' interceptors. Check out ky ’s doc to make use of its other features and configurations. A big shout out to Sindre for making such a nice library! 🎉 Give it a ⭐ if you enjoy it!

Next time I will share my experience on how to configure ky to include Authorization header for JWT authentication so stay tuned!

Thanks for reading and happy coding! 💻