Magic Methods in JavaScript? Meet Proxy!

With the landing of Proxy in es2015, aka es6, we can now extend even further the capabilities of the language, adding Magic Methods, that exists in various languages, using meta programming.


I have a project that consume a Rest API and I want to make some kind of a wrapper around the API.

For the sake of this example I’ll use the great JSON Placeholder website:
https://jsonplaceholder.typicode.com/

I want to make the API expressive, so I can reach any endpoint, existing or new, in a way like so:

api.posts.then(console.log);
api.comments.then(console.log);

Get Trap

To achieve that we can set some traps on a handler Object to Proxy operations made on our target api Object . Let’s write a get trap that will be called each time a property will be accessed on our target.

const target = {};
const handler = {
  get(target, name) {
    console.log(name);
  },
};

const api = new Proxy(target, handler);

api.posts; // => 'posts'
api.comments; // => 'comments'

This is very cool, using Proxy we were able to capture a simple property on an Object using the get Trap. This enable us to make some kind of abstraction to our API Rest wrapper.


Prepare the wrapper

Looking at what we got so far, we can return a Promise based on the property name and send this as an http request:

import axios from 'axios';

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com/';

const target = {};
const handler = {
  get(target, name) {
    return axios.get(name);
  },
};

const api = new Proxy(target, handler);
api.posts.then(console.log); // => axios response object

You can check it out here:
https://www.webpackbin.com/bins/-L-meFXGqKKWfpAJ9qYO

That’s cool, but it is far of being enough, we cannot add query params, make other http methods, posting a body etc.


Support other methods

So I wanted the wrapper’s api to look something like this:

api.posts.get().then(console.log);
api.posts.post(body).then(console.log);

We can return from the Trap something like that:

const handler = {
  get(target, name) {
    return {
      get() {
        return instance.get(name);
      },

      post(body) {
        return instance.post(name, body);
      },
    };
  },
};

That’s nice. but we still missing the ability to pass a more verbose url, like /posts/1/comments. We can add it like so:

const handler = {
  get(target, name) {
    return {
      get(url) {
        return instance.get(name + url);
      },

      post(url, body) {
        return instance.post(name + url, body);
      },
    };
  },
};

What about Query String? axios enable us to pass a params Object and construct the query string for us:

const handler = {
  get(target, name) {
    return {
      get(url, params) {
        return instance.get(name + url, { params });
      },

      post(url, body, params) {
        return instance.post(name + url, body, { params });
      },
    };
  },
};

Now let’s refactor it a bit, using a reducer supporting all desired methods:

const axios = require('axios');

const instance = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com/',
});

const target = {};
const handler = {
  get(target, name) {
    return Object.assign(
      {},
      [
        'get',
        'delete',
        'head',
      ].reduce(
        (o, method) => Object.assign({}, o, {
          [method](url = '', params = {}) {
            if (typeof url === 'object') {
              params = url;
              url = '';
            }

            return instance[method](name + url, { params });
          },
        }), {}),
      [
        'post',
        'put',
        'patch',
      ].reduce(
        (o, method) => Object.assign({}, o, {
          [method](url = '', body = {}, params = {}) {
            if (typeof url === 'object') {
              params = body;
              body = url;
              url = '';
            }

            return instance[method](name + url, body, { params });
          },
        }), {}),
    );
  },
};

const api = new Proxy(target, handler);

Now we can do something like this:

const response = ({ data }) => console.log(data);

api.posts.get().then(response);
api.posts.get({ userId: 10 }).then(response);
api.posts.get('/1/comments', { email: 'Lew@alysha.tv' }).then(response);

const body = {
  title: 'test',
  body: 'lorem ipsum',
  userId: 10,
};

api.posts.post(body).then(response);

You can mess with it here:
https://www.webpackbin.com/bins/-L-mo2Wrh6wd5xgtHmwZ

Thank you for your interest!

We will contact you as soon as possible.

Send us a message

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com