DEV Community

Yogesh Kumar
Yogesh Kumar

Posted on

`Proxy` for Dynamic Response Messages in TypeScript

Proxy for Dynamic Response

Most people want to build APIs or internal tools to return response messages in a consistent structure. For example, response like success, error, or warning follow a similar shape but differing only in status.

Some basic response message formats:

{
    status: "success",
    message: "user details updated successfully!"
}
{
    status: "error",
    message: "Oops! something went wrong."
}
Enter fullscreen mode Exit fullscreen mode

These functions can be written in a very simple way that anyone can understand it easily (check some examples below). But this often leads to code redundancy, and lack of flexibility.

function success(message) {
  return { status: "success", message };
}
function error(message) {
  return { status: "error", message };
}
Enter fullscreen mode Exit fullscreen mode

As you can see, this approach is hard to maintain, if you want to make some changes in the structure. You will have to update every function to keep the structure consistent.

To make this easy, I have introduced a solution using Proxy object in JavaScript. But before this, let's understand how Proxy works.

What is Proxy?

In JavaScript, Proxy is a powerful feature that allows you to customize the behaviour of operations performed on objects. Like property access, assignment, enumeration, and more.

A Proxy wraps an object and let you allow to redefine how basic operations work on that object. It takes two arguments:

const proxy = new Proxy(target, handler);
Enter fullscreen mode Exit fullscreen mode
  • target: the original object you want to wrap (can be empty object {}).
  • handler: an object that defines functions (called traps) for intercepting operations on the target object like get, set, apply, etc.

Basic Example

Here's a simple Proxy that returns a default value ("N/A") when trying to access a property that doesn't exist.

const themeSettings = { theme: "light", color: "blue" };

const themeWithDefault = new Proxy(themeSettings,
    {
        get: (target, prop) => 
            prop in target ? target[prop] : "N/A"
    })

console.log(themeWithDefault.theme); // Output: 'light'
console.log(themeWithDefault.fontSize); // Output: 'N/A'
Enter fullscreen mode Exit fullscreen mode

In this example you can see get trap intercepts property access.

Proxy Based Solution

Let's understand how we can use a Proxy to create dynamic response messages.

function createResponseMessage(message) {
  return new Proxy(
    {},
    {
      get: (_, status) => () => ({
        status,
        message,
      }),
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

In this function:

  • createResponseMessage returns a proxy object.
  • Accessing any key like .success, .error, etc., returns a consistent response object with message and status properties.

How to use this?

const res = createResponseMessage(
    "User updated successfully"
).success();

console.log(res); 
// Output: { status: "success", message: "User updated successfully" }
Enter fullscreen mode Exit fullscreen mode

As you can see, this approach creates a consistent response object in a clean, DRY, and elegant manner.

However, there is one major problem. To understand it better, let's look at another example:

const res = createResponseMessage(
    "Wrong status created!"
).problem();

console.log(res); 
// Output: { status: "problem", message: "Wrong status created!" }

Enter fullscreen mode Exit fullscreen mode

Did you notice the issue?

That's right! you can create any random status by accessing random property.

There are two way to fix this issue.

First, we can add a check to verify the status and throw an error if user tries to call it with an unsupported one.

function createResponseMessage(message) {
  const validStatuses = ["success", "info", "warning", "error"];
  return new Proxy(
    {},
    {
      get: (_, status) => () => {
        if (!validStatuses.includes(status)) {
          throw new Error("Invalid status!");
        }
        return ({
          status,
          message,
        })
      },
    }
  );
}

Enter fullscreen mode Exit fullscreen mode

This solution works but it is no longer as clean and elegant. Plus, you won't get suggestions for valid statuses. You will have to remember everything.

Right? So let's move on to the next solution.

Most developers today use TypeScript in their projects and our next solution takes full advantage of it.

To make it work, we will define two types: MessageStatus and ResponseMessage.

// type to define valid statuses
type MessageStatus = "success" | "info" | "warning" | "error";

// type to define the response structure
type ResponseMessage = {
  status: MessageStatus;
  message: string;
}
Enter fullscreen mode Exit fullscreen mode

Use these specified types in our solution:

function createResponseMessage(message: string) {
  return new Proxy(
    {},
    {
      get: (_, status: MessageStatus) => (): ResponseMessage => ({
        status,
        message,
      }),
    }
  ) as Record<MessageStatus, () => ResponseMessage>;
}
Enter fullscreen mode Exit fullscreen mode

Now you get full autocomplete and type safety.

This approach also helps to make your code cleaner and more expressive. You should also experiment with Proxy for handling dynamic API calls.

I would love to hear your thoughts! Let me know in comments.

If you have questions, feedback, or just want to connect, feel free to reach me here:

Always happy to talk about JavaScript, TypeScript, or clean code practices!

Thanks for reading!

Top comments (0)