A universal API client for JavaScript
Sounds tempting, right?
Fair warning, this is a bad idea.
Proxy
The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object.
You can create an handler for any operation on an object, which can act as a middleware / proxy sort of thing.
Why would you want to do this?
- Logging - you can log property access of an object
- Maybe modify/validate values ? Well that can also be done in the actual method
Example to log property access
const data = {
name: 'John Doe',
dob : '1990-01-01',
password: 'sUperS3cret'
}
const handler = {
get(target, prop, receiver) {
console.log(`Getting ${prop} from ${target}`)
if(prop === 'password'){
throw new Error("Password is being accessed");
}
return target[prop];
}
}
const proxy = new Proxy(data, handler);
console.log(proxy.name);
console.log(proxy.dob);
console.log(proxy.password); And here is what you get (I’m using Bun to run this)
Getting name from [object Object]
John Doe
Getting dob from [object Object]
1990-01-01
Getting password from [object Object]
6 |
7 | const handler = {
8 | get(target, prop, receiver) {
9 | console.log(`Getting ${prop} from ${target}`)
10 | if (prop === 'password') {
11 | throw new Error("Password is being accessed");
^
error: Password is being accessed
at get (/home/ajrx/Random/proxy/index.js:11:13)
at /home/ajrx/Random/proxy/index.js:21:13
Bun v1.1.33 (Linux x64) Now this might seem cool if you are not familiar with classes and private properties.
Now I am sure there are a lot more better uses for this, but I was just trying to show what Proxy can do.
Universal API Client - the holy grail \s
Let’s say you have a REST API that you want to use in your JavaScript application.
You have to build the URL, add the headers, add the body, call fetch and then parse the response.
Now let’s say you have good client built for your API. Then you can do this.
const client = new APIClient();
client.users().get();
client.users("user-ffh3sk213").delete();
client.users("user-ffh3sk213").update({ name: "John" });
client.users("user-ffh3sk213").comments().get();
// Pretty convenient, right? Now, let’s say you are still developing the API, things are changing and you are feeling too lazy to update your client. What if you could just use the same client for all your APIs?
Well, let’s try building one.
Lets with with a url builder
const store = {
path: [],
}
const handler = {
get(target, prop, receiver) {
switch (prop) {
case 'execute':
return () =>{
console.log('URL : /' + target.path.join("/"))
};
default:
target.path.push(prop)
return (arg) => {
if(arg){
target.path.push(arg)
}
return receiver;
};
}
}
}
const api = new Proxy(store, handler) Now we can use this to build our URLs
api.users('user-ffh3sk213').comments().execute(); URL : /users/user-ffh3sk213/comments And now we have a url builder. Note, the url builder is relying on a store that is shared ( same reference ), thus the path array remains filled after it executes. Better would be to have a new store for each instance.
const client = () => {
const store = {
path: []
};
const handler = {
get(target, prop, receiver) {
switch (prop) {
case 'execute':
return () => {
console.log(target.path.join("/"))
};
default:
target.path.push(prop)
return (arg) => {
if (arg) {
target.path.push(arg)
}
return receiver;
};
}
}
};
return new Proxy(store, handler)
}
client().users("id-13dasdpo2sm").comments().execute();
client().users().execute(); Lets make the client more useful with proper methods by modifying the handler
const handler = {
get(target, prop, receiver) {
switch (prop) {
case 'get':
return () => {
console.log(`GET : ${target.path.join("/")}`)
};
case 'create':
return (data) => {
console.log(`POST : ${target.path.join("/")}`)
console.log(JSON.stringify(data, null, 2))
};
case 'update':
return (data) => {
console.log(`PUT : ${target.path.join("/")}`)
console.log(JSON.stringify(data, null, 2))
};
case 'delete':
return () => {
console.log(`DELETE : ${target.path.join("/")}`)
};
default:
target.path.push(prop)
return (arg) => {
if (arg) {
target.path.push(arg)
}
return receiver;
};
}
}
}; And now we can use it
client().users("id-13dasdpo2sm").comments().get();
client().users().create({
name: "John Doe",
email: "john@doe.com"
});
client().satelites("dbaa08fd-bfd5-4a1b-b68a-885547468d3a").delete(); GET : users/id-13dasdpo2sm/comments
POST : users
{
"name": "John Doe",
"email": "john@doe.com"
}
DELETE : satelites/dbaa08fd-bfd5-4a1b-b68a-885547468d3a You can just add some fetch calls in the handler to actually have the api working, maybe enclose the client in a class to handle common data like auth headers, etc.
So, should you use this ?
Well, you have no type safety, no auto completions, no guarantees to be honest. Just thought this was cool, and can always be useful if you are feeling lazy or your backend is always changing.