Creating HTTP Client service for React-Redux app

Reading Time: 4 minutes

Unlike Angular, React does not have a standard library for making HTTP requests, and also there is no standard way of organizing all the HTTP call functions. So in this blog, I will share a way of creating an HTTP client service using the good old “fetch()” API method for a Standard React-Redux web app. This is what I have used and it worked great for all my React projects.

Benefits of creating an HTTP Client service class in react:

  • Reduces duplicate code for writing fetch() method again and again.
  • All methods for making HTTP calls organized in one service improve readability.
  • It’s scalable to add new and modify the function used for making HTTP requests.

What is service? What exactly are we trying to build?

Services are nothing but simple javascript classes that contains some functionality. And other functions and components in our app can simply use these services when provided to them.
We want to build a class that has all the methods we want to use to make HTTP requests to our REST API server. And provide this HTTP client as an “extraArgument” in react redux-thunk middleware to use in our action creators.

Important Note:

Highly recommend using typescript, it reduces the chances of bugs and typos. The development experience is just awesome with the superpower of creating types and interfaces.
For the blog I will be using the JS only.

Prerequisite:

  • Knowledge of Object Oriented JS
  • Knowledge of React with Redux and Redux Thunk.

Creating a fetch() with authorization

It is pretty common that the backend APIs will be protected and we will need to attach an authorization header with each HTTP request. So it will be awesome if we create a generic wrapper function that returns the fetch() method with an attached auth header.

It is a common practice to store user auth details in the redux store. We will now create a function that takes in Redux State as an argument, get the user auth token from the state object, and returns a callback function that takes in URL and options an argument and return fetch() with authorization header.

export const getFetchWithAuth = (state) => {
  const authToken = state.user.token;

  if (!authToken) {
    throw new Error('no auth token');
  }

  return (url, options) => fetch(url, {
    ...options,
    headers: {
      authorization: `Bearer ${authToken}`,
      ...options.headers,
    },
  });
};

We will now use fetch() returned from this rather than normal fetch().

Creating the HTTP Client Service

The HTTP client Service class will contain a “request” method as its property which will have the predefined options for the HTTP call like the header, origin/hostname, protocol, etc. It will take an options argument which will have the endpoint, method, and other headers according to the specific endpoint. The “request” method of this service will use the custom fetch() we built earlier.

export class $HttpClient {
  private customFetch;

  constructor(customFetch) {
    this.customFetch = customFetch;
  }

  public request(options){
    const origin = 'https://jsonplaceholder.typicode.com'

    const finalUrl = origin + endpoint

    const finalHeaders = {
      accept: 'application/json',
      'content-type': 'application/json',
      ...options.headers,
    };

    const request = {
      body: options.body,
      headers: finalHeaders,
      method: options.method,
      url: finalUrl,
    };

    return this.customFetch(request.url, {
      body: JSON.stringify(request.body),
      headers: request.headers,
      method: request.method,
    }).then((response) => {
      return (response.json()).then((json) => {
        return {
          body: json,
          headers: response.headers,
          ok: response.ok,
          request,
          status: response.status,
          statusText: response.statusText,
        };
      });
    });
  }
}

So as you see with this we do the mundane repetitive work associated with all fetch() methods one time. We just now need to create a service class specific to an API endpoint that can use this HTTP client service.

Creating Resource service classes for specific endpoints

To clarify what a Resource is, suppose we create a web app to create and read posts online, add friends and chat with them. So our backend API will have an endpoint for fetching, creating, updating deleting Posts. Another endpoint for adding friends and viewing their profiles.
So a group of methods used for interacting with an endpoint is a resource.

Parent class for all resources:

export class $Resource {
  protected client;

  constructor(fetchWithAuth) {
    this.client = new $HttpClient(fetchWithAuth);
  }
}

Let’s create a service for Posts resource

export class PostsResource extends $Resource {
  public get() {
    return this.client.request({
      endpoint: `/posts`,
      method: 'GET',
    });
  }

  public getById(param) {
    return this.client.request({
      endpoint: `/posts/${param.id}`,
      method: 'GET',
    });
  }

  public post(params) {
    return this.client.request({
      body: params.body,
      endpoint: `/posts`,
      method: 'POST',
    });
  }

  public put(params) {
    return this.client.request({
      body: params.body,
      endpoint: `/posts/${param.id}`,
      method: 'PUT',
    });
  }
}

So like this all the other Resources you might have can be created.

Creating the providing the ApiClient service

Now we need to create the one ultimate service class that will contain all the resources and which we can pass as the extra argument in redux-thunk middleware.

export class ApiClient {
  postsResource;
  
  constructor(fetchWithAuth) {
     this.postsResource = new PostResource(fetchWithAuth);
  }
}

Now to provide this ApiClient. We can use the redux-thunk extraArgument

Now since we will need the redux state to create the fetchWithAuth and then pass it to the ApiClient class, we will make the extraArgument a function that takes the redux state as an argument, and returns the ApiClient service class with the custom fetch() method.

Modifying the configure store method

const configureStore = (initialState) => {
  const thunkWithExtraArgument = reduxThunk.withExtraArgument({
     api: (state) => new ApiClient(getFetchWithAuth(state))
  });

  const store = createStore(
    (state, action) => syncCurrentSelections(rootReducer(state, action), action),
    initialState,
    composeWithDevTools(
      applyMiddleware(
        thunkWithExtraArgument,
      ),
    ),
  );

  return store;
};

Using ApiClient in action creators

export default const fetchPosts() => async (
  dispatch,
  getState,
  thunkExtraArgs,
) => {
  dispatch({
    type: PostActionTypes.FETCH_POSTS_REQUEST,
  });

  const result = await thunkExtraArgs.api(getState()).postsResource.get();

  if (result.status === 200) {
    dispatch({
      type: PostActionTypes.FETCH_POSTS_SUCCESS,
      payload: result.body,
    });
    return result.body;
  }
  dispatch({
    type: PostActionTypes.FETCH_POSTS_FAILURE,
  });

  return undefined;
};

Conclusion

That’s it, as simple as that, we saw one way to create a pro-level HTTP Service for your react-redux app. A bit of work to lay a solid foundation with lead to a more robust application and development process. Using this template you can modify it according to your project and make it even more efficient. I would love to know your thought on how it can be improved.

Important resources:

https://redux.js.org/usage/writing-logic-thunks

Leave a Reply