Stop writing API functions
Tags: javascript, webdev, codequality, productivity
Published: 2022-09-17 23:22:15

If you are developing a front-end application which uses a back-end with RESTFUL APIs, then you have got to stop writing functions for each and every endpoint!

RESTFUL APIs usually give you a set of endpoints to perform CRUD (Create, Read, Update, Delete) actions on different entities. We usually have a function in our project for each of these endpoints and these functions do a very similar job, but for different entities. For example, let's say we have these functions:

```js
// apis/users.js

// Create
export function createUser(userFormValues) {
  return fetch("/users", { method: "POST", body: userFormValues });
}

// Read
export function getListOfUsers(keyword) {
  return fetch(`/users?keyword=${keyword}`);
}

export function getUser(id) {
  return fetch(`/users/${id}`);
}


// Update
export function updateUser(id, userFormValues) {
  return fetch(`/users/${id}`, { method: "PUT", body: userFormValues });
}

// Destroy
export function removeUser(id) {
  return fetch(`/users/${id}`, { method: "DELETE" });
}
```

and a similar set of functions may exist for other entities like `City`, `Product`, `Category`, ... . However, we can replace all of these functions with a simple function call:

```js
// apis/users.js
export const users = crudBuilder("/users");

// apis/cities.js
export const cities = crudBuilder("/regions/cities");

```

And then use it like this:

```js
users.create(values);
users.show(1);
users.list("john");
users.update(values);
users.remove(1);
```

## "But Why?" you may ask
Well, there are some good reasons for that:
- It reduces the lines of code  -  code that you have to write and someone else has to maintain when you leave the company.
- It enforces a naming convention for the API functions, which increases code readability and maintainability. You may have seen function names like `getListOfUsers`, `getCities`, `getAllProducts`, `productIndex`, `fetchCategories` etc. that all do the same thing: "get the list of an entity". With this approach you'll always have `entityName.list()` functions and everybody on your team knows that.

So, let's create that `crudBuilder()` function and then add some sugar to it.

---

## A very simple CRUD builder

For the simple example above, the `crudBuilder` function would be so simple:

```js
export function crudBuilder(baseRoute) {
  function list(keyword) {
    return fetch(`${baseRoute}?keyword=${keyword}`);
  }

  function show(id) {
    return fetch(`${baseRoute}/${id}`);
  }

  function create(formValues) {
    return fetch(baseRoute, { method: "POST", body: formValues });
  }

  function update(id, formValues) {
    return fetch(`${baseRoute}/${id}`, { method: "PUT", body: formValues });
  }

  function remove(id) {
    return fetch(`${baseRoute}/${id}`, { method: "DELETE" });
  }
  
  return {
    list,
    show,
    create,
    update,
    remove
  }
}
```

It assumes a convention for API paths and given a path prefix for an entity, it returns all the methods required to call CRUD actions on that entity.

But let's be honest, we know that a real-world application is not that simple! There are lots of thing to consider when applying this approach to our projects:

- **Filtering:** list APIs usually get lots of filter parameters
- **Pagination**: Lists are always paginated
- **Transformation:** The API provided values may need some transformation before actually being used
- **Preparation:** The `formValues` objects need some preparation before being sent to the API
- **Custom Endpoints:** The endpoint for updating a specific item is not always `` `${baseRoute}/${id}` ``

So, we need a CRUD builder that can handle more complex situations.

---

## The Advanced CRUD Builder

Let's build something that we can really use in our everyday projects by addressing the above issues.

### Filtering

First, we should be able to handle more complicated filtering in out `list` function. Each entity list may have different filters and the user may have applied some of them. So, we can't have any assumption about the shape or values of applied filters, but we can assume that any list filtering  can result in an object that specifies some values for different filter names. For example to filter some users we could have:

```js
const filters = {
  keyword: "john",
  createdAt: new Date("2020-02-10"),
}
```
On the other hand, we don't know how these filters should be passed to the API, but we can assume (and have a contract with API providers) that each filter has a corresponding parameter in the list API which can be passed in the form of `"key=value"` URL query params.

So, we need to know how to transform the applied filters into their corresponding API parameters to create our `list` function. This can be done with passing a `transformFilters` parameter to the `crudBuilder()`. An example of that for users could be:

```js
function transformUserFilters(filters) {
  const params = []
  if(filters.keyword) {
    params.push(`keyword=${filters.keyword}`;
  }
  if(filters.createdAt) {
    params.push(`created_at=${dateUtility.format(filters.createdAt)}`;
  }
  
  return params;
}
```

Now, we can use this parameter to create the `list` function.

```js
export function crudBuilder(baseRoute, transformFilters) {
  function list(filters) {
    let params = transformFilters(filters)?.join("&");
    if(params) {
      params += "?"
    }
    return fetch(`${baseRoute}${params}`);
  }
}
```

### Transformation and Pagination

The data received from the API may need some transformation before being usable in our app. For example we may need to transform the `snake_case` names to `camelCase` or to transform some date string into users timezone etc.
Also, we need to handle pagination. 

Let's assume that all paginated data from APIs have the following shape (Standardized by the API provider):

```js
{
  data: [], //list of entity objects
  pagination: {...}, // the pagination info
}
```
So all we need to know is how we should transform a single entity object. Then we can loop over the objects of the list to transform them. To do that, we need a `transformEntity` function as a parameter to our `crudBuilder`:

```js
export function crudBuilder(baseRoute, transformFilters, transformEntity) {
  function list(filters) {
    const params = transformFilters(filters)?.join("&");
    return fetch(`${baseRoute}?${params}`)
      .then((res) => res.json())
      .then((res) => ({
        data: res.data.map((entity) => transformEntity(entity)),
        pagination: res.pagination,
      }));
  }
}
```

And we are done with the `list()` function.


### Preparation

For `create` and `update` functions, we need to transform the `formValues` into the format that the API expects. For example, imagine we have a city select in our form that selects a `City` object. but the create API only needs the `city_id`. So, we would have a function that does something like this:

```js
const prepareValue = formValues => ({city_id: formValues.city.id}) 

```
This function may return a plain object or a `FormData` depending on the use-case and can be used to pass data to API like:

```js
export function crudBuilder(
  baseRoute,
  transformFilters,
  transformEntity,
  prepareFormValues
) {
  function create(formValues) {
    return fetch(baseRoute, {
      method: "POST",
      body: prepareFormValues(formValues),
    });
  }
}
```

### Custom Endpoints

There are some rare situations that the API endpoint for some action on an entity doesn't follow the same convention. For example instead of having `` `/users/${id}` `` to edit a user, we have to use `` `/edit-user/${id}` ``. For these cases we should be able to specify a custom path. 

Here, we allow the override of any path used in crud builder. Note that the paths for show, update and remove actions may depend on some info from the entity object, so we have to use a function and pass the entity object to get the path.

We need to get these custom paths in an object and fallback to our default paths if nothing specified. Something like:

```js
const paths = {
  list: "list-of-users",
  show: (userId) => `users/with/id/${userId}`,
  create: "users/new",
  update: (user) => `users/update/${user.id}`,
  remove: (user) => `delete-user/${user.id}`
}
```

## The Final CRUD Builder

This is the final code to create CRUD API functions.

```js
export function crudBuilder(
  baseRoute,
  transformFilters,
  transformEntity,
  prepareFormValues,
  paths
) {

  function list(filters) {
    const path = paths.list || baseRoute;
    let params = transformFilters(filters)?.join("&");
    if(params) {
      params += "?"
    }
    return fetch(`${path}${params}`)
      .then((res) => res.json())
      .then((res) => ({
        data: res.data.map((entity) => transformEntity(entity)),
        pagination: res.pagination,
      }));
  }

  function show(id) {
    const path = paths.show?.(id) || `${baseRoute}/${id}`;

    return fetch(path)
      .then((res) => res.json())
      .then((res) => transformEntity(res));
  }
  
  function create(formValues) {
    const path = paths.create || baseRoute;

    return fetch(path, {
      method: "POST",
      body: prepareFormValues(formValues),
    });
  }

  function update(id, formValues) {
    const path = paths.update?.(id) || `${baseRoute}/${id}`;

    return fetch(path, { method: "PUT", body: formValues });
  }

  function remove(id) {
    const path = paths.remove?.(id) || `${baseRoute}/${id}`;
    
    return fetch(path, { method: "DELETE" });
  }

    return {
    list,
    show,
    create,
    update,
    remove
  }
}
```