Build a REST client
Tags: webdev, javascript, tutorial, learning
Published: 2024-02-26 01:48:12

A **REST client** is an application that lets you interact with RESTful web services. It acts as a go-between for you to send HTTP requests and get responses from RESTful APIs.

You can build REST clients using different programming languages like Python, Java, Ruby, and more. They're commonly used in web development and can be integrated into applications to allow communication between different systems.

The main advantage of using a REST client is that it simplifies the process of sending HTTP requests and getting responses. It provides an easy-to-use interface for interacting with RESTful APIs without worrying about the technical details.

In this post, we'll explore how to create a REST client using JavaScript Proxy. But before we dive into that, let's take a look at the common task of fetching data from remote servers.

## Retrieving data

To illustrate the technique, we'll be using the service provided by [jsonplaceholder](https://jsonplaceholder.typicode.com/). It offers fake APIs for prototyping and testing purposes.

In the example below, we'll use the `fetch` function to retrieve all the posts.

```js
fetch('https://jsonplaceholder.typicode.com/posts')
    .then((response) => response.json())
    .then((json) => console.log(json));
```

In this example, we use the HTTP GET method to send a request to the server. We make the request using the `fetch` function, a built-in JavaScript function that allows us to make network requests. We pass in the URL of the API endpoint we want to access, which in this case is `https://jsonplaceholder.typicode.com/posts`.

After sending the request, we chain two `.then()` methods. The first `.then()` method takes the response object returned by the server and converts it into easy-to-read JSON format using its `json()` method. We then return this JSON data from our `.then()` callback function.

In the second `.then()` method, we take the JSON data returned from the first `.then()` method and log it to the console using `console.log()`. This allows us to see all of the post data returned by the API endpoint in our browser's developer tools console.

## Retrieving post details with the id parameter

Similarly, here is a sample code snippet that retrieves the details of a specific post based on its ID:

```js
fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then((response) => response.json())
    .then((json) => console.log(json));
```

To retrieve the details of a specific post, we need to include the post ID as a parameter in the API endpoint URL. In the sample code provided, we use `https://jsonplaceholder.typicode.com/posts/1` as our API endpoint URL. The `1` in `/posts/1` represents the post ID that we want to retrieve.

When we send this request to the server using `fetch`, it returns only the details of the post with ID 1. You can change the value of `1` in the URL to any other valid post ID to retrieve its details.

## Simplifying REST API requests

Are you tired of dealing with the tedious boilerplate code when using `fetch` and `json` functions? Well, I have good news for you! We'll be creating a function that simplifies this process by invoking these functions under the hood.

Introducing the `createClient` function - a JavaScript function that returns a REST client. All you need to do is provide the base URL of the RESTful API as the argument.

```js
const createClient = (url) => {
    return new Proxy({}, {
        get(target, key) {
            return async function(id = "") {
                const response = await fetch(`${url}/${key}/${id}`)
                return response.ok
                    ? response.json()
                    : Promise.resolve({ error: "Invalid request" });
            };
        },
    });
};
```

This function creates a new `Proxy` object that acts as a way to interact with a RESTful API. The `get` method of the proxy object intercepts any property access on the client object and returns an asynchronous function that sends HTTP requests to the corresponding endpoint.

When you access a property on the client, it returns an asynchronous function that can take an optional `id` parameter. This parameter represents the ID of the resource you want to retrieve from the server. If no `id` is provided, then all resources are returned.

To send HTTP requests to our RESTful API endpoint, we use the `fetch` API inside the asynchronous function. We concatenate our base URL with our endpoint name and optionally with an ID if it was passed in as a parameter.

If our server sends a successful response (HTTP status code 200-299), then we convert our response data into JSON format using its `.json()` method. The JSON data is then returned from our async request.

If an error occurs while sending or receiving data from the server, then we return a Promise resolved with an object containing only one key "error" and its value "Invalid request".

Now, let's try using the client to fetch data from jsonplaceholder.

```js
const client = createClient("https://jsonplaceholder.typicode.com");
```

To retrieve all the posts, all we have to do is make a simple call:

```js
const posts = await client.posts();

// [
//      { userId: 1, id: 1, title: '...', body: '...' },
//      { userId: 1, id: 2, title: '...', body: '...' },
//      ...
//      { userId: 2, id: 11, title: '...', body: '...' },
//      ...
//      { userId: 10, id: 100, title: '...', body: '...' },
// ]
```

When we use the `client.posts()` function, the `get` method of the proxy object steps in and returns an asynchronous function. This function sends a GET request to the `/posts` endpoint of our RESTful API, which returns all available posts on the server.

To get the details of a specific post, we simply pass the post's ID to the `posts()` function.

```js
const post = await client.posts(1);

// {
//      userId: 1,
//      id: 1,
//      title: '...',
//      body: '...',
// }
```

When we use the `client.posts(1)` method, the proxy object's `get` method intercepts it and returns an asynchronous function. This function sends a GET request to our RESTful API's `/posts/1` endpoint and retrieves the details of the post with ID 1.

If we pass a different valid post ID, it will retrieve the details of that post instead. However, if we pass an invalid or non-existent ID, the function will return a Promise that resolves with an object containing an "error" key and the value "Invalid request".

## Using query parameters in API endpoints

In many cases, we need to include additional query parameters in the API endpoint URL. By doing this, we can filter posts based on specific criteria such as title, body, or user ID.

For instance, if we want to retrieve all posts written by a user with an ID of 2, we can send a GET request to `https://jsonplaceholder.typicode.com/posts?userId=2`.

To support query parameters, we can modify the `createClient` function by adding an optional `params` object parameter. This object will contain all of the query parameters we want to pass in the API endpoint URL.

```js
const createClient = (url) => {
    return new Proxy({}, {
        get(target, key) {
            return async function(id = "", params = {}) {
                let queryString = "";
                if (Object.keys(params).length > 0) {
                    const queryParams = new URLSearchParams(params);
                    queryString = `?${queryParams.toString()}`;
                }
                const response = await fetch(`${url}/${key}/${id}${queryString}`);
                return response.ok
                    ? response.json()
                    : Promise.resolve({ error: "Invalid request" });
            };
        },
    });
};
```

In this updated version of the `createClient` function, we've made some improvements. We now check if there are any query parameters in our `params` object.

If there are, we use a handy tool called the `URLSearchParams` class in JavaScript to convert our parameters into a query string. Then, we simply add this query string to the end of our API endpoint URL using template literals.

For instance, let's say we want to fetch all posts written by user 2:

```js
const postsByUser = await client.posts("", { userId: 2 });

// [
//      { userId: 2, id: 11, title: '...', body: '...' },
//      { userId: 2, id: 12, title: '...', body: '...' },
//      ...
//      { userId: 2, id: 20, title: '...', body: '...' },
// ]
```

To retrieve all posts written by a user with ID 2 from our RESTful API, we simply call `client.posts("", { userId: 2 })`. This sends a GET request to `/posts?userId=2` endpoint, which filters the results and returns all posts written by that user.&#x20;

By passing multiple query parameters as an object, we can easily filter the results we get back from the server.

## Expanding HTTP method support

Up until now, our client has only supported the GET method. But did you know that there are more HTTP methods available, such as POST, PUT, and PATCH? That means we can do even more with our client! To specify which method to use, simply define it in the `fetch` function.

```js
fetch(url, {
    method: 'POST',
});
```

Here's an example of how you can create a new post using the `fetch` function:

```js
fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    body: JSON.stringify({
        title: 'foo',
        body: 'bar',
        userId: 1,
    }),
    headers: {
        'Content-type': 'application/json; charset=UTF-8',
    },
})
    .then((response) => response.json())
    .then((json) => console.log(json));
```

To support different HTTP methods, we'll be making some changes to the API, including the specified HTTP method.

```js
const client = createClient("...");

// Get all posts
const posts = await client.posts.get();

// Get a single post
const post = await client.posts(1).get();

// Get posts written by a given user
const postsByUser = await client.posts().get({ userId: 2 });
```

Here's an example of what a POST request might look like.

```js
const newPost = await client.posts().post({
    title: 'foo',
    body: 'bar',
    userId: 1,
});
```

To enable support for different HTTP methods, we can update the `createClient` function to return a new object containing functions that correspond to each of the methods.

```js
const createClient = (baseUrl, paths = []) => {
    const callable = () => {};
    callable.url = baseUrl;

    return new Proxy(callable, {
        get({ url }, prop) {
            const method = prop.toUpperCase();
            const path = paths.concat(prop);
            if (!['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
                return createClient(`${url}/${prop}`, path);
            }

            return (data) => {
                const payload = {
                    method,
                    headers: {
                        'Content-type': 'application/json; charset=UTF-8',
                    },
                };
                switch (method) {
                    case 'GET':
                        if (data) {
                        url = `${url}?${new URLSearchParams(data)}`;
                        }
                        break;
                    case 'POST':
                    case 'PUT':
                    case 'PATCH':
                        payload.body = JSON.stringify(data);
                        break;
                }
                return fetch(url, payload).then((result) => result.json());
            };
        },
        apply({ url }, thisArg, [arg] = []) {
            const path = url.split('/');
            return createClient(arg ? `${url}/${arg}` : url, path);
        },
    });
};
```

The updated `createClient` function returns an object with functions for each HTTP method. When we call any method on this object, it sends an HTTP request to the specified URL and returns the response.

We use the `Proxy` object to intercept property access and return the appropriate asynchronous function. If the property is not one of the supported HTTP methods, we create a new client with a new URL path and return it. This allows us to chain multiple calls together.

For supported HTTP methods, we create a new payload object with properties such as method, headers, and body. We use the built-in `URLSearchParams` class to convert query parameters into a string and append them to the endpoint URL.

We send the request using the `fetch` API and wait for the response. If there are no errors, we parse the JSON data and return it as a resolved Promise. If there are errors, we return a resolved Promise with an error message.

There's an important point we need to discuss about the `apply` handler in the updated version of the `createClient` function. This handler is responsible for creating a new client object with a URL path that includes any extra segments passed as arguments to the function.

Here's an example: if we call `client.posts(1)`, it returns a new client object with the URL path `/posts/1`. Then, if we call `.comments()`, it returns another new client object with the URL path `/posts/1/comments`.

This way, we can chain multiple calls together and create complex API endpoints without having to manually construct long URL strings.

The `createClient` function has been updated to support additional HTTP methods including POST, PUT, PATCH, and DELETE.

For instance, if you want to retrieve all posts using the GET method, simply make the following call:

```js
const posts = await client.posts.get();
```

To retrieve a specific post using the GET method, we can make a call using an ID parameter like so:

```js
const post = await client.posts(1).get();
```

To retrieve posts written by a specific user using the GET method with query parameters, we can use the following code:

```js
const postsByUser = await client.posts().get({ userId: 2 });
```

Alternatively, we can create a new post using the POST method by calling:

```js
const newPost = await client.posts().post({
    title: 'foo',
    body: 'bar',
    userId: 1,
});

// {
//      id: 101,
//      title: "foo",
//      body: "bar",
//      userId: 1,
// }
```

In the same way, we can use the PUT or PATCH method to modify an existing post. And when we want to delete a post from our server, we can simply use the DELETE method.

```js
await client.posts(1).delete();
```

With the updated `createClient` version, interacting with RESTful APIs that support more than just GET requests becomes a breeze. Now, all HTTP methods are supported and requests can be made by calling them directly on the client object. It's that simple!

## Conclusion

To sum it up, using JavaScript Proxy to create a REST client is a simple and elegant way to interact with RESTful APIs. Instead of worrying about low-level networking code, we can focus on building our application logic.

Our `createClient` function lets us define a base URL and API paths, and supports various HTTP methods. This means we can easily construct complex API endpoints and query parameters by chaining multiple calls together.

JavaScript Proxy is a powerful feature that helps us create flexible and composable abstractions in our code. By using it, we can write cleaner and more maintainable code that is easier to understand.

---

If you found this series helpful, please consider giving the [repository](https://github.com/phuocng/javascript-proxy) a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

If you want more helpful content like this, feel free to follow me:

-   [DEV](https://dev.to/phuocng)
-   [GitHub](https://github.com/phuocng)
-   [Website](https://phuoc.ng)