DEV Community

Cover image for Build a REST client
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Build a REST client

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. It offers fake APIs for prototyping and testing purposes.

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

fetch('https://jsonplaceholder.typicode.com/posts')
    .then((response) => response.json())
    .then((json) => console.log(json));
Enter fullscreen mode Exit fullscreen mode

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:

fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then((response) => response.json())
    .then((json) => console.log(json));
Enter fullscreen mode Exit fullscreen mode

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.

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" });
            };
        },
    });
};
Enter fullscreen mode Exit fullscreen mode

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.

const client = createClient("https://jsonplaceholder.typicode.com");
Enter fullscreen mode Exit fullscreen mode

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

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: '...' },
// ]
Enter fullscreen mode Exit fullscreen mode

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.

const post = await client.posts(1);

// {
//      userId: 1,
//      id: 1,
//      title: '...',
//      body: '...',
// }
Enter fullscreen mode Exit fullscreen mode

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.

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" });
            };
        },
    });
};
Enter fullscreen mode Exit fullscreen mode

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:

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: '...' },
// ]
Enter fullscreen mode Exit fullscreen mode

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.

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.

fetch(url, {
    method: 'POST',
});
Enter fullscreen mode Exit fullscreen mode

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

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));
Enter fullscreen mode Exit fullscreen mode

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

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 });
Enter fullscreen mode Exit fullscreen mode

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

const newPost = await client.posts().post({
    title: 'foo',
    body: 'bar',
    userId: 1,
});
Enter fullscreen mode Exit fullscreen mode

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.

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);
        },
    });
};
Enter fullscreen mode Exit fullscreen mode

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:

const posts = await client.posts.get();
Enter fullscreen mode Exit fullscreen mode

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

const post = await client.posts(1).get();
Enter fullscreen mode Exit fullscreen mode

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

const postsByUser = await client.posts().get({ userId: 2 });
Enter fullscreen mode Exit fullscreen mode

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

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

// {
//      id: 101,
//      title: "foo",
//      body: "bar",
//      userId: 1,
// }
Enter fullscreen mode Exit fullscreen mode

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.

await client.posts(1).delete();
Enter fullscreen mode Exit fullscreen mode

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 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:

Top comments (0)