DEV Community

Cover image for A Simple Approach to Managing API Calls
Michael McShinsky
Michael McShinsky

Posted on Edited on Originally published at magitek.dev

A Simple Approach to Managing API Calls

In my article, "Architecting Frontend Projects To Scale", we took a look at organizing our frontend code base in a way to make scaling and succeeding as a team much easier. In this article we're going to take a small dive into the services layer of our code organization. Specifically, we will look at a simple solution for managing 3rd party APIs or our own data sources in such a way that will help us avoid some of the frustrations with managing our code base as APIs change over time.

When we first start building out features, most of us tend to dump all feature logic into a single component. The database calls, state management, and all the child components that are managed or display the data which we are presenting to the end user are located here. As a result of doing this, we begin to create a very bloated set of files that consume, manage, and present all the logic as it becomes more complex with the increase in business logic. What may have started out as simple CRUD (Create, Read, Update, Delete) actions will inevitably grow into a multitude of specialized functions and intertwined business logic. If we are not careful in our code architecture design process, we may find ourselves locked into function dependencies that are so messy that we even fear the refactoring process because we do not want to create a single bug that may have us working over the weekend to fix.

Avoiding The Mess

One part of this business logic mess that we can avoid is to not hard code our API calls into our components directly. Our goal is to abstract everything related to API logic into our services layer in order to make our components a little more lean and maintainable. This concept directly aligns itself with Dan Abramov's article "Presentational and Container Components" as well as creating a Model/Service layer in our frontend framework to abstract most business logic away from our reusable components.

Here is a simple example of what you may start out with:

import React, { useEffect } from 'react';
import axios from 'axios';

let API_URL_TASKS = 'https://url.com/api/v1/tasks';

export function Tasks() {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    _getTasks();
  }, []);

  function _getTasks() {
    axios
      .get(API_URL_TASKS)
      .then((res) => {
        let arr = _parseTasks(res.results.data);
        setTasks(arr);
      })
      .catch((err) => {
        _handleError(err, type);
      });
  }

  function _parseTasks(tasks) {
    return tasks.map((task) => {
      // Parse task information
      return task;
    });
  }

  function _createTask(task) {
    axios
      .post(url, task)
      .then((res) => {
        _handleSuccess(res, 'post');
        // etc...
      })
      .catch((err) => {
        _handleError(err, 'post');
      });
  }

  function _updateTask(task) {
    let url = `${API_URL_TASKS}/${id}`;
    axios
      .patch(url, task)
      .then((res) => {
        _handleSuccess(res, 'patch');
        // etc...
      })
      .catch((err) => {
        _handleError(err, 'patch');
      });
  }

  function _removeTask(id) {
    let url = `${API_URL_TASKS}/${id}`;
    axios
      .delete(url)
      .then((res) => {
        _handleSuccess(res, 'delete');
        // etc...
      })
      .catch((err) => {
        _handleError(err, 'delete');
      });
  }

  function _handleSuccess(response, type) {
    // success message
    // actions against state with type
  }

  function _handleError(error, type) {
    // error message
    // actions based on type
    // etc...
  }

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>{task.name}</li>
      ))}
    </ul>
  );
}

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung ⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung

As you can see, our component's data flow is directly related and hardcoded to one or many API endpoints that it may require. If you start to do this with many components over time, and your API requirements change from the server or 3rd party API, you have now cornered yourself into the painful process of finding all instances that need to be changed in order to avoid code and interface failure for your end user. Instead, we're going to create a few file structures in our service layer in order to make it easier to maintain changes over time.

my-app 
└── src
    ├── components
    ├── views
    |   └── tasks
    └── services
        ├── api
        |   ├── tasks
        |   └── utilities
        ├── model
        |   └── task
        └── etc...
⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung ⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung

Service Utilities

In the services folder, we're going to create a few utilities to make our APIs reusable and standardized for all components and team members. We'll be making use of the JavaScript axios library and JavaScript classes in this example to create our API utilities.

services
└── api
    └── utilities
        ├── core.js
        ├── index.js
        ├── provider.js
        └── response.js
⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung ⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung

We're going to focus on three main files here:

  1. provider.js - Defines how axios or any api library should connect with the database and connect our response data back to any connected file or component.
  2. core.js - Defines the reusable class that makes use of our provider.js with options we can define per api endpoint collection. As a result of being a constructor function, we can extend it's functionality on individual API collections as needed while still keeping a consistent base for the majority of our code.
  3. response.js - Middleware to handle response parsing, error handling, logging, etc...

Provider.js

// provider.js

import axios from 'axios'; 
import { handleResponse, handleError } from './response'; 

// Define your api url from any source.
// Pulling from your .env file when on the server or from localhost when locally
const BASE_URL = 'http://127.0.0.1:3333/api/v1'; 

/** @param {string} resource */ 
const getAll = (resource) => { 
  return axios 
    .get(`${BASE_URL}/${resource}`) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {string} id */ 
const getSingle = (resource, id) => { 
  return axios 
    .get(`${BASE_URL}/${resource}/${id}`) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {object} model */ 
const post = (resource, model) => { 
  return axios 
    .post(`${BASE_URL}/${resource}`, model) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {object} model */ 
const put = (resource, model) => { 
  return axios 
    .put(`${BASE_URL}/${resource}`, model) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {object} model */ 
const patch = (resource, model) => { 
  return axios 
    .patch(`${BASE_URL}/${resource}`, model) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {string} id */ 
const remove = (resource, id) => { 
  return axios 
    .delete(`${BASE_URL}/${resource}`, id) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

export const apiProvider = { 
  getAll, 
  getSingle, 
  post, 
  put, 
  patch, 
  remove, 
};
⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung ⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung

Core.js

In this constructor class, we can define which base API resources will be consumed. We can also extend the class in each API utility to include custom endpoints unique to the API table(s) without created accidental one-off solutions littered in our code base away from this file.

// core.js

import apiProvider from './provider';

export class ApiCore {
  constructor(options) {
    if (options.getAll) {
      this.getAll = () => {
        return apiProvider.getAll(options.url);
      };
    }

    if (options.getSingle) {
      this.getSingle = (id) => {
        return apiProvider.getSingle(options.url, id);
      };
    }

    if (options.post) {
      this.post = (model) => {
        return apiProvider.post(options.url, model);
      };
    }

    if (options.put) {
      this.put = (model) => {
        return apiProvider.put(options.url, model);
      };
    }

    if (options.patch) {
      this.patch = (model) => {
        return apiProvider.patch(options.url, model);
      };
    }

    if (options.remove) {
      this.remove = (id) => {
        return apiProvider.remove(options.url, id);
      };
    }
  }
}
⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung ⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung

Response.js

This is kept separate to keep our files lean and allow a clean separation for any response and error logic you may want to handle here for all API calls. Maybe you want to log an error here or create custom actions for authorization based on the response header.

// response.js

export function handleResponse(response) {
  if (response.results) {
    return response.results;
  }

  if (response.data) {
    return response.data;
  }

  return response;
}

export function handleError(error) {
  if (error.data) {
    return error.data;
  }
  return error;
}
⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung ⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung

Individual APIs

We can now extend our base api class to make use of all the api configurations that will be used for any api collection.

// Task API

const url = 'tasks';
const plural = 'tasks';
const single = 'task';

// plural and single may be used for message logic if needed in the ApiCore class.

const apiTasks = new ApiCore({
  getAll: true,
  getSingle: true,
  post: true,
  put: false,
  patch: true,
  delete: false,
  url: url,
  plural: plural,
  single: single
});

apiTasks.massUpdate = () => {
  // Add custom api call logic here
}

export apiTasks;
⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung ⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung

Implementing Our Changes

Now that we have our setup complete, we can import and integrate our api calls into multiple components as needed. Here is an updated Task component with our changes.

import React, { useEffect } from 'react';

import { apiTasks } from '@/services/api';

export function Tasks() {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    _getTasks();
  }, []);

  function _getTasks() {
    apiTasks.getAll().then((res) => {
      let arr = _parseTasks(res.results.data);
      setTasks(arr);
    });
  }

  function _parseTasks(tasks) {
    return tasks.map((task) => {
      // Parse task information
      return task;
    });
  }

  function _createTask(task) {
    apiTasks.post(task).then((res) => {
      // state logic
    });
  }

  function _updateTask(task) {
    apiTasks.patch(task).then((res) => {
      // state logic
    });
  }

  function _removeTask(id) {
    apiTasks.remove(id).then((res) => {
      // state logic
    });
  }

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>{task.name}</li>
      ))}
    </ul>
  );
}

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung ⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung

Conclusion

With a little extraction of code into reusable service utilities, our app can now manage API changes much easier. A failed API call can now be addressed in one location, it's implementation can easily tracking, and our component dependencies can be quickly updated to reflect the change in data flow and manipulation. I hope this helps you manage your API structure in such a way as to make your code not only sustainable in the long run but easily managed and understood as your code base and team grows!

Here is a link to the collection of files discussed in this article: Gist Link


If you found this helpful or useful, please share a 💓, 🦄, or 🔖. Thanks!

Top comments (31)

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
sumukhesh profile image
Sumukhesh •

Great read. As a junior developer it helped understand the importance of building scalable architecture. Its so easy to just bundle everything up in single file but it surely has long term repercussions. I have experienced it.
Cheers !!

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
wulymammoth profile image
David • • Edited

Hi Michael -- interesting post.

What is prefix in your examples? I'm unsure because prefix usually implies "placed before", but it comes after the base URL in your examples. Suffix?

Would what you've outlined here be better described as a clear separation of concerns between the data gathering and the view logic? Furthermore, the components themselves are unconcerned about how it acquires its data so long as it gets them?

I do like the provider abstraction, though! However, the downside, from what I can tell, is that it is a light wrapper around Axios. I think the methods defined there are typically a part of what I describe below that's found in your core logic. I think you're trying to enforce an interface (which is good), but the downside of this, is more providers aren't added, so whatever pattern has been established means almost nothing at all. However, if there certainly are, this idea that you're capturing is known as the strategy pattern, that often comes up for anyone that's worked with an OAuth library to offer delegated auth.

I think what you've named, ApiCore or core is akin to the repository pattern -- providing an interface to perform CRUD operations against a resource.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky • • Edited

You're right, prefix would be better changed to endpoint to make it more descriptive.

This can be considered a light wrapper to axios but can really be changed to any provider you want pretty easily.

Thanks! I appreciate your experience and feedback!

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
wulymammoth profile image
David •

An endpoint usually encompasses the entire URL. If you're up for a suggestion, mine would be resource in the canonical API and RESTful nomenclature sense. The resource that you're using as an example appears to be tasks resources.

Fair enough. If I added or created a new provider, rather than swapping/modifying existing code and possibly introducing a regression -- I'm typically for keeping legacy things around, doing a partial release to end users for something new, address any bugs, then switch over and remove the old. I like your emphasis on clean architecture which alludes me to the ideas surrounding "Clean Code" by "Uncle Bob" who is often attributed for the SOLID acronym. You may enjoy this repository: github.com/ryanmcdermott/clean-cod...

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung Thread
 
mikemcshinsky profile image
Michael McShinsky •

I like resource better, thanks. Especially since this abstraction directly address the resource in question in addition to any custom endpoints related to the resource. Good catch.

Thanks for the article! Looks like a really good read. 😄

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung Thread
 
wulymammoth profile image
David •

Happy coding, Michael! And thanks for sharing. These sorts of pieces keep me in the loop in JS land that I haven't worked in for about two years :)

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung Thread
 
himanshu2454 profile image
Himanshu Chouhan •

Thanx for sharing the link David

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
angha_ramdohokar_0b6505c2 profile image
Angha Ramdohokar •

Hi Michael, this article helped me understand the abstraction layer you have added over APIs middleware.
I have implemented this in my recent project.
Thanks for sharing.
Great read.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
biapar profile image
BP •

How to check if the api return an error or data?
Here for example:
function _createTask(task) {
apiTasks.post(task).then((res) => {
// state logic
});
}

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky •

Add a .catch after your .then

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
biapar profile image
BP •

Don't work, because it goes always into the 'then' branch. I think this logic issue is caused by core.js that return the with the same instruction the good or bad value.

.getAll()
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err.code);
});

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung Thread
 
mikemcshinsky profile image
Michael McShinsky •

I can't say for certain since there are various scenarios in which your code doesn't register the error as a thrown error, bad response, etc... of which I don't know what your code is.

Example: Many users assume a 4xx error response from axios should throw an error and be caught by the .catch. This is not the case unfortunately and you have to update the axios configurations to change how response codes are handled.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung Thread
 
biapar profile image
BP •

Ok, I tried to switch off the api server and the pointer goes in the catch in the api core but the high level api get the error in the then branch. How to different the bad/good response?

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
dyeplucban profile image
Globoy • • Edited

I got an error on exporting individual api when creating a custom function.

but if directly export it, it works. I don't know what's wrong.

EXPORT ERROR

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky •

You'll either want to on line 3 export const skillsApi or on line 17 export default skillsApi.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
kevinpallado profile image
Kevin Pallado •

hi did you try to create customCall? can you show me how you done

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky •

task-api-example.js has one. You can add your axios logic there.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
jeffskynird profile image
HvIvON •

the best way to implement AbortController to cancel requests??, only pass as param?

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky •

Yeah, you definitely could customize your api implementation to cater to that.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
kevinpallado profile image
Kevin Pallado •

can you provide any example for custom call api?

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky •

task-api-example.js has one. You can add your axios logic there.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
xdiepx profile image
Diep Dao •

what is the index.js for?

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky •

A typical pattern for exporting all files within a folder instead of having a bunch of different imports is done via an index.js. You import all sibling files to this file and then export from there.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
xdiepx profile image
Diep Dao •

oh i see

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
xdiepx profile image
Diep Dao •

Thanks, btw thanks for the tip above really helped me

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
biapar profile image
BP •

Where Do you put the "header" axios config ?

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky •

One option is to put it (in this example) in the provider.js file.

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
asaduzzaman69 profile image
Asaduzzaman Himel •

Great Post. I am wondering where should i put my Login and signup handler. Because they are not reusable but they are related to api functionality. Any suggestion!?

⚽ Live Match: mmcshinsky/a simple approach to managing api calls lo - Jadwal Siaran Langsung
 
mikemcshinsky profile image
Michael McShinsky •

Yeah, you could add them in as part of the auth (namespaced) portion of your api functionality.