DEV Community

Cover image for Make fetch better and your API request methods easier to implement
Simon Ström
Simon Ström

Posted on

Make fetch better and your API request methods easier to implement

In this post, I will share my thought on how I like to structure methods on how to get data from your REST API backend. The idea here is to show the basics of my idea, and then you should add, remove, and adopt the code to your specific needs.

This method will also be useful in what ever framework or platform you are in like React, Angular, Vue, or even NodeJS (with a few polyfills, for example fetch...)!

Oh, and a little heads up. We are going object-oriented programming. So a basic understanding of javascript classes and the fetch api would be good before you keep going.

The end game

In the end, we will be able to request data from our backend like this to assign a users variable:

users = await backend.users.get()
Enter fullscreen mode Exit fullscreen mode

Instead of something like this:

const res = await fetch('/users', {
  headers: {
    Authorization: '********',
    lang: 'en'
  }
})

if(!res.ok) throw new Error(res.statusText)

users = await res.json()
Enter fullscreen mode Exit fullscreen mode

Motivation

So why go through this process? Well, first of all, it will make your code easier to read. You will get all the code from fetch hidden behind explanatory method calls. Like backend.get.users(), that makes sense and is short.

Sure you could extract the logic into a function named getUsers() and make the fetch call in there. But then here is the next benefit: don't repeat your self. Without a good structure for your back end requests, you will definitely repeat your self. Setting authorization and other headers in multiple places, or just to see if the fetch call is ok everywhere...

You could also move this code outside into a library for use in your web apps and Node services without a hassle.

Lets get started

The code can be found here.

So we will start of by making our own "mini, mini, mini version" of Axios (or insert name of http client here):

class HttpClient {
  constructor(options = {}) {
    this._baseURL = options.baseURL || "";
    this._headers = options.headers || {};
  }
}
Enter fullscreen mode Exit fullscreen mode

We start things off with the constructor, we will accept two options when instantiating the class:

Base URL will be used to construct the URL. Later we will use a get method like this get('/users') and if we defined the baseURL with https://jsonplaceholder.typicode.com the request URL will be https://jsonplaceholder.typicode.com/users

Headers will be a default set of headers that will be sent with every request.

If you think, "what is with the _ before property names?" Well, I intend these properties to be private. Meant only to be used inside this (or inherited) classes. Not from anywhere outside in the actual app code. Soon there will be a syntax for this as well that implements the behavior, check this out

We should probably add some way of setting headers as well:

setHeader(key, value) {
    this._headers[key] = value;
    return this;
  }
Enter fullscreen mode Exit fullscreen mode

At the end of the setHeader method I added return this. This is added so we can chain method calls. For example when instantiating the HttpClient class:

const httpClient = new HttpClient({baseURL: 'xxx'})
                     .setBasicAuth("user", "pass")
                     .setHeader("lang", "en")

Enter fullscreen mode Exit fullscreen mode

Now in the example above I used another method setBasicAuth. I will skip that for now, but at the end of this post, you will find some inspiration for more properties and things you can add to the client.

Lets make requests!

This will be done in two steps. First of all we will define our own wrapper function for fetch, and then we will make separate methods for get/post/put/delete/patch:

async _fetchJSON(endpoint, options = {}) {
  const res = await fetch(this._baseURL + endpoint, {
    ...options,
    headers: this._headers
  });

  if (!res.ok) throw new Error(res.statusText);

  if (options.parseResponse !== false && res.status !== 204)
    return res.json();

  return undefined;
}
Enter fullscreen mode Exit fullscreen mode

So this wrapper function just makes fetch behave a little more like I want it to do in this specific use case. Like the fact that fetch does not throw on bad requests.

The first argument is just the endpoint (string), and if we set the baseURL option it will be relative to that.

And the options argument is just the fetch Request object we can add additional properties to. It can be empty, but more information about available properties is found here.

Oh! But I do append the parseResponse options to this argument to tell whether the response should be parsed to JSON or not. For most of my use cases, I want to opt-out of the parsing. So if left blank the parsing is done if not the API explicitly states No Content.

You could argue that we could check the content length or some other stuff, but what is good is that if I do require a response and I say I wanted it parsed. This method will throw if I do not get the response. So it will blow up here instead of in my application later where it might take me longer to find the cause.

Now lets expose some ways of making request. This should hopefully be straight forward:

get(endpoint, options = {}) {
  return this._fetchJSON(
    endpoint, 
    { 
      ...options, 
      method: 'GET' 
    }
  )
}

post(endpoint, body, options = {}) {
  return this._fetchJSON(
    endpoint, 
    {
      ...options, 
      body: JSON.stringify(body), 
      method: 'POST' 
    }
  )
}

delete(endpoint, options = {}) {
  return this._fetchJSON(
    endpoint, 
    {
      parseResponse: false,
      ...options, 
      method: 'DELETE' 
    }
  )
}

/** AND SO ON */
Enter fullscreen mode Exit fullscreen mode

We simply call our _fetchJSON method and set some options to make the HTTP method match our method name, and maybe set a body correct so that is taken care of.

Now we could just do some API calls:

const httpClient = new HttpClient({baseURL: 'https://example.com'})
                     .setHeader('lang', 'sv')

const users = await httpClient.get('/users')
Enter fullscreen mode Exit fullscreen mode

One step further: The API client

We have done a lot! This client is our own "mini, mini, mini-version" of Axios. That we easily can extend with whatever parameters, options, or functions we would need.

But I want to take it one step further, I want to define our back end API with easy to call methods. Like I mentioned in the beginning. Now we can take one of two approaches. We could just add more methods to the HttpClient directly, and keep on working.

But, this class does serve its purpose now, right? It can work on its own, and be useful that way. So what if we let the HttpClient class be our base class and we can inherit that one to create our ApiClient class.

This way we can make other HttpClients for talking to other services by using the HttpClient class directly. And talking to our backend with the ApiClient class, which just adds to the existing functionality.

Inheriting the HttpClient class would look like this:

import HttpClient from "./http-client"

class ApiClient extends HttpClient { 
  constructor(baseURL, langCode) {
    super({
      baseURL,
      headers: {
        lang: langCode
      }
    });
  }

  get users() {
    return {
      get: () => this.get("/users"),
      delete: (id) => this.delete(`/users/${id}`),
      create: (user) => this.post("/users", user),
      update: (user) => this.put(`/users/${user.id}`, user)
    };
  }
}

export default ApiClient
Enter fullscreen mode Exit fullscreen mode

Well, that was quite fast. We just added our little flavor to the constructor and we could simple and quickly define our endpoints.

And adding additional endpoints is now really simple and reliable.

Take it further

Now, this was a quick way to add basic functionality and then extending this to make the specific client.

The idea here is to make the base as simple as possible and then add every feature you need instead of bringing in the full capacity of an external library upfront.

Some things you could do next, if applicable to your needs of course:

Add helpers to authenticate if you do not rely on cookies

For example, if you need basic authentication:

setBasicAuth(username, password) {
  this._headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`
  return this
}
Enter fullscreen mode Exit fullscreen mode

Just remember that btoa is not available globally in NodeJS. But just polyfill it and you will be good to go.

And for a bearer auth:

setBearerAuth(token) {
  this._headers.Authorization = `Bearer ${token}`
  return this
}
Enter fullscreen mode Exit fullscreen mode

Make the functions generic if you are on typescript

I do like typescript, 90% of all my code is in typescript. When building this for typescript add a generic return type to the functions, and for your post methods you should type the expected body:

_fetchJSON<T = any>(endpoint: string, options: RequestInit = {}):Promise<T>{ /**/ }
Enter fullscreen mode Exit fullscreen mode

I usually do a little hack when the body is not parsed and say return undefined as any to just get Typescript to not complain. If you expect undefined T should be undefined and you are good to go.

And in your API client for a post request like this:

users = {
  post: (user:IUser) => this.post<IUser>('/users', user)
}
Enter fullscreen mode Exit fullscreen mode

Add methods to any applicable header your API expects/can use

For example at work we have a header to include or exclude empty values in the response (to save some time transferring large collections)

includeEmptyAndDefault(shouldInclude) {
  if(shouldInclude) {
   this._headers.IncludeEmptyAndDefault = 1
  } else {
   this._headers.IncludeEmptyAndDefault = 0
  }
  return this
}
Enter fullscreen mode Exit fullscreen mode

Build a package

Use Rollup or tsdx, if you are into typescript, if you want to do a separate package. This way the API client can be used as a module in other projects as well. That can be great for you, and your customers, to make things happen fast.

But as I said, only add what you need. If you have any thoughts please share them in the comments and recommend me any patterns you like if this was not your cup of tea.

Cover photo by Florian Krumm on Unsplash

Top comments (16)

Collapse
 
gaidygomez profile image
gaidyjg

Great post. This makes me remember the functionalitys of packages HttpClient of PHP. This is very useful for simplify of the handle http request's. Thanks for the tutorial.

Collapse
 
stroemdev profile image
Simon Ström

Cool! My experience with PHP is limited though. The inspiration is from my background doing C# and their httpclient class. Glad you liked it!

Collapse
 
raghavmisra profile image
Raghav Misra

The C# HTTPClient is exactly what I was thinking about while reading this. Great post!

Thread Thread
 
stroemdev profile image
Simon Ström

Its always good to look at other languages some times to find inspiration. Thank you!

Collapse
 
srikanth597 profile image
srikanth597

Nice post

Collapse
 
stroemdev profile image
Simon Ström

Thanks, glad you enjoyed it!

Collapse
 
inacior profile image
Renan Inácio

Nice way to use classes in JS, with extends

Collapse
 
stroemdev profile image
Simon Ström

Yeah, when looking at docs for classes all examples of extending is these things like animal extending to dog or car extending to Honda. So it is Nice with a more "real world" context for extending.

Collapse
 
opimand profile image
opimand

Oh, good tutorial. You save a lot my time 👍

Collapse
 
stroemdev profile image
Simon Ström

Awesome! More time spent doing fun stuff

Collapse
 
fellipegpbotelho profile image
Fellipe Geraldo Pereira Botelho

Nice article!

Collapse
 
stroemdev profile image
Simon Ström

Thank you!

Collapse
 
marcbaumbach profile image
Marc Baumbach

Nice article! The HttpClient in your example code is using "POST" for the the HTTP method when calling the "put" function on it. Great little wrapper around fetch, though. :)

Collapse
 
stroemdev profile image
Simon Ström

Oh! Always good with an extra pair of eyes on your code, thank you :-)

Collapse
 
leonavevor profile image
leonavevor

Nice and cool

Collapse
 
stroemdev profile image
Simon Ström

Thanks!