How to manage large number of similar REST calls in JS/TS?

The problem:

  • I have more than 30 functions (and growing) that make REST calls to various API endpoints.
  • I keep all of these functions in one large file.
  • These functions follow a similar pattern of making a fetch() and passing url, body etc.

Question

I would like to achieve some degree of abstraction to lessen the amount of code and use some modern design patterns that would help me in the long run. What should I preferably do to achieve this?

  • Should I use class-based architecture and JS Class, and if so, how?
  • Should I make use of core JS with Function.prototype.call, Function.prototype.apply or Function.prototype.bind, and if so, how?
  • Is there some other method by which I can achieve it? Any tips and illustrative examples are greatly appreciated.

Examples of functions

Here is a flavour of the different functions that I am working with.

Note, in the fetch function I am already passing an instance of a class called RequestInit to lessen the amount of boilerplate code.

Simple function without parameters:

export async function getLocale() {
  const url = endpoints.LOCALE_URL;
  try {
    const response = await fetch(url, new RequestInit(Method.Get));
    return response;
  } catch (error) {
    throw new Error(
      `There was an error fetching locale.\n${error}`
    );
  }
}

Function with parameter as fetch body:

export async function getLikes(params: LikeParams): Promise<Response> {
  if (Array.isArray(params.like_types)) {
    params.like_types = params.like_types.join(",");
  }
  const url = endpoints.LIKES_URL;
  const body = params;
  try {
    const response = await fetch(url, new RequestInit(Method.Get, body));
    return response;
  } catch (error) {
    throw new Error(
      `There was an error fetching likes.\n${error}`
    );
  }
}

Function with parameter as url path:

export async function getAsset(symbol: string): Promise<Response> {
  const url = `${endpoints.ASSETS_URL}/${symbol}`;
  try {
    const response = await fetch(url, new RequestInit(Method.Get));
    return response;
  } catch (error) {
    throw new Error(
      `There was an error fetching asset information for ${symbol}.\n${error}`
    );
  }
}

There are various ways to do that. First of course, you can create a base function to fetch and then have various functions to make specific calls, or pass in some flag or enum (since you mentioned TS) that tells it which call to make. (I prefer the former). You could put these all in one file where you handle this.

Of course, you could also wrap this in a class. You could create a singleton pattern and the class has a private base method and for each call you need, you hardcode the values you can and pass in what you can’t.

Which is better? Meh. I think it depends - if you are an OOP guy, then you probably prefer the class. But I think either one works. The difference is largely conceptual and semantic. I think the point is to get everything into one place, out of the way, and don’t make the calling locations have to think about anything they don’t have to think about.

Hi Kevin,

Thank you for your reply @kevinSmith .

I think I get what you mean by base function that does the try - fetch - catch part. Then I create separate function where I used that base function. This seems simple and straight forward and is perhaps the preferred method.

Would it be too much to ask you to explain what you mean by “singleton pattern” and perhaps give a simple illustrative example? I’m fairly new to programming.

I think I get what you mean by base function that does the try - fetch - catch part. Then I create separate function where I used that base function. This seems simple and straight forward and is perhaps the preferred method.

Yeah, I might do something like:

const _baseRequest = async ({ url, method, errorMessage }) => {
  try {
    const response = await fetch(url, new RequestInit(method));
    return response
  } catch (error) {
    throw new Error(
      `${errorMessage}\n${error}`
    );
  }
}

export const getLocale = async () =>
  _baseRequest({
    url: endpoints.LOCALE_URL,
    method: Method.Get,
    errorMessage: 'There was an error fetching locale.',
  })

Maybe you also need to pass in parameters. Maybe you need to pass in variables for your URL - you could define your URL with replaceable placeholders like:

https://my.url/get-user/{{userID}}

and then pass in a URL parameters object with a property “userID” and search and replace “{{userID}}” in the URL with what is passed in. That could be in your base function. You may want to have separate base functions for GET and POST - I probably would. I might have a _basePost that calls _baseRequest. But there are different ways to handle those things and different philosophies.

Then all you need to do is import getLocale where you need it. You never import the *baseRequest stuff because no one needs to know about it. They are essentially private. You just have a one file/module where all the API request business is housed and no one else has to worry about it.

Would it be too much to ask you to explain what you mean by “singleton pattern” and perhaps give a simple illustrative example? I’m fairly new to programming.

Wrapping it in a class is not that much different, it’s mainly a conceptual difference. The main “problem” is that every time you use “new” you are creating a new instance of the class. It may not be a big problem but it is a bad practice if you don’t need different instances. There is now reason for different instances of your API module. So, we ensure that there is only one instance, a “singleton”.

We might have:

class APIModule {
  private static instance: APIModule

  private constructor() {}

  public static getInstance(): APIModule {
    if (!APIModule.instance) {
      APIModule.instance = new APIModule()
    }

    return APIModule.instance
  }


  private async baseRequest({ url, method, errorMessage }) {
    try {
      return await fetch(url, new RequestInit(method))
    } catch (error) {
      throw new Error(`${errorMessage}\n${error}`)
    }
  }
  
  public async getLocale = () {
    this.baseRequest({
      url: endpoints.LOCALE_URL,
      method: Method.Get,
      errorMessage: 'There was an error fetching locale.'
    })
  }
}

export default APIModule

Then to use it somewhere, after importing the class:

const apiModule = APIModule.getInstance()
const locale = apiModule.getLocale()

By using getInstance, the class can ensure that there is only one instance. The constructor was declared as private so it can’t be instantiated anywhere else - only inside the class. getInstance is declared as static since it needs to be used on the class, not a specific instance.

I wrote this with TS in mind - the annotation gets a little weird. Or course it needs some more annotation, but that is the basic idea.

I’m doing this all off the top on my head - I haven’t tried this - but this is the idea.

Hi @kevinSmith ,

Thank you so much for your answer. I learned a great deal from your examples.

Would it be correct to say that classes are instantiated globally? If so, I did not know about it and I incorrectly used my custom class called RequestInit by using the new keyword.

Also, a small question of why you prefix the base class with _?

I’m not sure what you mean by “instantiated globally”. They are instantiated in the scope in which they are instantiated. They are variables so they have the same scope as any other variable in JS. You know you are instantiating it when you use new.

Also, a small question of why you prefix the base class with _ ?

I didn’t prefix any class with that. The only class I created was APIModule.

The function inside the first example (which has no classes) I prefixed with an underscore. This is kind of a message to other developers that “this function is meant to be private to this module”. JS modules do not have a way to make things private. It doesn’t matter much here because it’s not exported, but I might have to export it for test suites. I don’t think JS has a way to make those truly private - a standard way to tell people that is to prefix it with an underscore. But that is not required and not everyone does that.

With classes it isn’t a big deal. In TS you can use the private keyword and I think that in the latest JS you can put a # in front of members/methods you want to keep private.

They are instantiated in the scope in which they are instantiated.

I am running my 30’ish functions with my class called RequestInit(...) inside the .fetch(...). Why do I need a singleton for it? I thought it wouldn’t be a problem since I assumed that only one class instance is instantiated when I run the function and then the same class instance is immediately deallocated from memory once the function returns. Maybe this is not why it is bad practice as you mentioned. Perhaps there is another reason.

The function inside the first example (which has no classes) I prefixed with an underscore. This is kind of a message to other developers that “this function is meant to be private to this module”.

I did not know that, thank you for telling me. I have seen this in multiple code bases but not known what it means before now.

By the way, I used your class approach above and created a module. The output is a lot cleaner and less typing.

Now I just need to read up on what singleton classes are.

It would depend on how you write your code, but it probably not. The class remains instantiated as long as there is a refernce to it - that has nothing to do with whether or not the function is run. And yo may have multiple sections of code creating their own instances.

I have seen this in multiple code bases but not known what it means before now.

Keep in mind that there are other uses for that. I’ve seen 8t used for dummy parameters (like when you only need the second parameter of a callback. Of course, there is also the lodash library that usually imported as just an underscore so you have code like _.forEach (…

Singleton is just a design pattern commonly usd with classes. We only need one API module. In some modules you only want one instance to everyone is using the same instance, like if you were storing app settings in a class instance - you don’t want everywhere in the app creating their own instance - you want just one instance so they are all changing and reading the same data. Of course, that assumes that you are using a class for that.

It would depend on how you write your code, but it probably not. The class remains instantiated as long as there is a refernce to it - that has nothing to do with whether or not the function is run. And yo may have multiple sections of code creating their own instances.

I now realise what you were doing with the getInstance and in the process understand what a singleton is!

What is the practice when it comes to _ inside a class member that is denoted with private. Would it be wrong to use it if one wants to make the point extra explicit?

With your 'instantiated globally" question, you could say that singleton classes almost act like global variables - it is the same reference to the same data everywhere. That is the point of the singleton.

I finally got it. It is a very good pattern to remember to write safe code.

A little odd, just a little redundant. Alan Flusser once said that a man that wears a belt and suspenders is his definition of a pessimist.

Andnif you are using TS, it’s going to scream at you if you try to access a private member publically anyway.

A little odd, just a little redundant. Alan Flusser once said that a man that wears a belt and suspenders is his definition of a pessimist.

I do not know if I am helped by using TypeScript. As a beginner, it is nice to have that type of safety and go through the motion of identifying each type. On the other hand, learning TypeScript on top of JavaScript adds more complexity for me.

I will follow the pro advice and not put _ before private.

With your 'instantiated globally" question, you could say that singleton classes almost act like global variables - it is the same reference to the same data everywhere. That is the point of the singleton.

How do singletons work?

Assume I have 3 files: A, B and C.

A - is exporting my singleton class.
B & C both want to use the singleton.

How would I make my instantiation of the singleton class available in both files?

Thanks again for all your help @kevinSmith

Not sure if this helps. The site has a bunch of other patterns you can look at as well.

Thank you Lasse! @lasjorg

I think that helped me. Basically, I need to instantiate it and export it as with anything else.

Even though I don’t know react, the site is very useful. I wish there was something like it not just for react but more generally for JavaScript.

There is pure JS as well. You would probably have found it digging around but here is the link to the (updated) original book.

1 Like

Perhaps you are already there, but in B and C you have:

import APIModule from 'whatever/path/APIModule'

// ...

const apiModule = APIModule.getInstance()

Then you can just use methods on that instance. The singleton pattern ensures that getInstance will always return just the one instance wherever it is called.

But if you are not comfortable with this pattern, then don’t worry about it. You can just use a file as a module and export the functions, like we did in the first example. The difference is just in design philosophy - they will accomplish the same thing.

As to TS, I just mentioned it because you did. I did the singleton with it because it is a bit odd with the annotations. But as pointed out, you can do it just fine with pure JS.

As to TS in general … at the risk of getting into opinion territory … I think TS is the wave of the future. It has been shooting up in popularity and “fixes” some of the “problems” with JS. I think any smart JS developer needs to learn TS.

I get that it is harder. But I would point out that those are just things that other languages have to do all the time. I think that TS typing, etc. is still simpler than Java. I think people have just gotten used to JS being the language that is really easy. But I think that also allows people to pick up a lot of bad habits.

Can you learn JS first? Sure. I think it is probably good to learn pure JS first, not worrying about annotation. But I think if you have reached the point where you are building modules, it’s time to start thinking about the structure and control that TS offers. Can you learn that without TS? Sure, I did. But then I had to unlearn some things. And learning how to properly annotate things helped me to learn it better.

That’s just my $.02. I’m sure there are some that disagree.