background

Photo by Ignacio Amenábar on Unsplash

Based on @DavidWells shared JS Proxy tweets and GitHub gist.

Last week, I discovered an elegant way to create simple javascript REST API clients using Proxy.

Javascript Proxy ?

Proxy object is well documented in the Mozilla MDN documentation : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

First API Client with JS Proxy

Let’s create our first client API.

In this example, we will use the JSONPlaceholder API (https://jsonplaceholder.typicode.com/).

Client :

function createClient (url) {
  return new Proxy({}, {
    get(_, key) {
      return async (id = "") => {
        const reqUrl = new URL(`${key}/${id}`, url);
        return fetch(reqUrl.toString()).then((d) => d.json());
      }
    }
  })
}

Usage :

// Create client instance
const client = createClient('https://jsonplaceholder.typicode.com');

// Call /todos endpoint
await client.todos();

// Get todo element /todos/<id>
await client.todos(1);

Explainations :

In this example, we have created a JS Proxy on an empty object.

We have also declared the get proxy method which contains the API Client logic.

When calling get(_, key), the key parameter contains the name of the resource to call.

Ex: on client.todos call, key parameter contains 'todos'.

To handle GET /resource/<id> calls, the get function return an asynchronous function with an id parameter.

That’s why you can do client.todos(<id>).

The corresponding API endpoint is called on this asynchronous function and the result is returned.

Advanced usage

With the same logic, I tried to implement a Gitlab API client.

https://gitlab.com/ziggornif/gitlab-client

Problems

The previous example only work with GET calls on first resources level.

Gitlab API has multiple resources levels (ex: https://gitlab.com/api/v4/users/00000/projects)

The API has POST/PUT endpoints.

Solution

GET POST PUT requests

To manage the different methods (GET, POST, PUT …), I created a processRequest function like the following.

function processRequest({url, method, headers, params, data}) {
  const query = { headers, method }
  const reqUrl = new URL(url)
  switch (method) {
    case 'GET':
      if (params) {
        for(const key of Object.keys(params)) {
          reqUrl.searchParams.set(key, params[key])
        }
      }
      break;
    case "POST":
    case "PUT":
    case "PATCH": {
      query.body = JSON.stringify(data);
      break;
    }
  
    default:
      break;
  }
  return fetch(reqUrl.toString(), query).then((d) => d.json());
}

This function handles all kinds of requests with body and / or query params.

Multiple resources levels calls (/resources//action)

To handle the multiple resources levels, I refactored the get Proxy method.

Now, the request is sent if key value is equal to an http verb (GET, POST, PUT…).

Otherwise, the get method concatenate the URL with the current key and return a new Proxy.

function createClient (url) {
  return new Proxy({}, {
    get(_, key) {
      const method = key.toUpperCase();
      if (["GET", "POST", "PUT", "PATCH"].includes(method)) {
        return async function({ data, params, headers } = {}) {
          return processRequest({ url, method, data, params, headers })
        }
      }
      return createClient(`${url}/${key}`, token);
    }
  })
}

With this solution, we can execute now more complex queries.

Example

// Get user projects
// https://gitlab.com/api/v4/users/00000/projects
const userProjs = await client.users[00000].projects.get();
console.log(userProjs);

Conclusion

JavaScript proxies can be really useful for creating API client libraries.

In addition, by writing these libraries in native JavaScript, they will be usable both on the backend side and on the frontend side.

Project example

You can retrieve and fork the complete project here : https://gitlab.com/ziggornif/gitlab-client