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
Your comments, suggestions and questions are most welcome!