I want to create a custom client for a restful API that follows the json:api specification. So, I created this simple client with async support:
MyTools.Api = (function () {
"use strict";
//#region Private
function ajax(url, method, data) {
return new Promise(function (resolve, reject) {
let request = new XMLHttpRequest();
request.responseType = 'json';
//Open first, before setting the request headers.
request.open(method, url, true);
request.setRequestHeader('Authorization', 'Bearer ' + window.headerToken)
request.onreadystatechange = function () {
if (request.readyState === XMLHttpRequest.DONE) {
if (request.status === 200) {
resolve(request.response);
} else {
reject(Error(request.status));
}
}
};
request.onerror = function () {
reject(Error("Network Error"));
};
request.open(method, url, true);
request.send(data);
});
}
//#endregion
//#region Public
return function (url, method, data) {
url = window.apiBasePath + url;
return ajax(url, method, data);
};
//#endregion
})(MyTools.Api || {});
I am passing the token from the back-end (.net) to a window global variable called headerToken. The same with the base path (apiBasePath). Now, I can call this client like like this
CTools.Api("/dashboard/users/", "GET").then(function (result) {
console.log(result);
});
My goal is to create a more fluent way to consume the api.For example,I want to call something like:
mytools.api.dashboard.users.get().then(function (result) {
console.log(result);
});
and
mytools.api.dashboard.users.get(fliteroptions).then(function (result) {
console.log(result);
});
and if there is another module to use like
mytools.api.basket.items.get(fliteroptions).then(function (result) {
onsole.log(result);
});
Dashboard and basket will have different urls. Both the client and the url building will be created inside mytools namespace. Also the variables headerToken and apiBasePath will be assigned in mytools constructor after a rest call.
What design pattern to use in this case? Keep in mind that I want a non-ES6 solution.
There's essentially only really two ways of doing what you want to do; either each route (dashboard
, basket
, etc) is a full api object with fetch
behaviour etc, or the get()
method in dashboard
etc maps back to api
.
Something like this:
class Route {
constructor(url, api) {
this.url = url;
this.api = api;
}
get(filterOptions) {
return this.api.get(this.url, filterOptions)
}
post(filterOptions) {
return this.api.post(this.url, filterOptions);
}
}
class Api {
constructor(basePath) {
this.basePath = basePath;
}
get(route, filterOptions) {
// filterOptions can be null here
return fetch(`${this.basePath}/${route}`)
.then(data => data.json())
}
post(route, filterOptions) {
// filterOptions can be null here
...
}
...
}
mytools.api = new Api();
mytools.api.dashboard = new Route('dashboard', mytools.api);
mytools.api.basket = new Route('basket', mytools.api);
...
Your Route
class is a simple class that holds a url
and whatever else, as well as the api
which will actually do the fetching.
Api
wraps your fetch
and handles any filterOptions
passed.
When you create your Api
, you also create all the routes.
The downside is that you need to know all of your routes beforehand.
To do it a bit more dynamically, you can look into Proxy
: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Here, you can implement a function that's called every time your object is accessed:
var handler = {
get: function(target, name) {
return "Hello, " + name;
}
};
var proxy = new Proxy({}, handler);
console.log(proxy.world); // output: Hello, world
You should be able to hook into it so that get
method returns the Api
object, and you can then call directly on it
Edit for ES5
It's been a while since I've written pure ES5, but converting the Route
class, for example, should look something like this:
function Route(url, api) {
this.url = url;
this.api = api;
}
Route.prototype.get = function(filterOptions) {
return this.api.get(this.url, filterOptions)
}
Route.prototype.post = function(filterOptions) {
return this.api.post(this.url, filterOptions)
}
Creating the objects should stay the same - new Route(url, api)
I'm not sure if fetch
is ES5 - you'd need to look at where the code will be running to see if it's supported or not. Ditto with promises; you'd either need to use a library like Bluebird
, or work with callbacks.
More info on classes here: https://medium.com/@apalshah/javascript-class-difference-between-es5-and-es6-classes-a37b6c90c7f8
Edit for dynamic URLs
If I've understood you correctly, you're getting the URLs from a REST call.
Assume that the return of this call is something like:
{
"dashboard": {
"url": "dashboard/",
"token": "..."
},
"basket": {
"url": "basket/",
"token": "..."
},
...
}
At this point, you can now go through and create your api:
mytools.api = new Api();
mytools.api.dashboard = new Route(jsonObj.dashboard.url, mytools.api);
mytools.api.basket = new Route(jsonObj.basket.url, mytools.api);
...
You can also pass in token
, or whatever else is in your REST JSON object. It does mean a few things though:
mytools.api.dashboard
etc until that initial REST call is made - it won't point to anythingmytools.api.dashboard
variableIn relation to point 2, you can't really have truly dynamique urls (i.e. you can add "profile"
to the REST JSON without needing to add a corresponding mytools.api.profile
call in your codebase) without rethinking how you approach things. What you're doing here (making the API easier to work with) is pre-compile - as in, when you're writing your code. However, your routes aren't known until runtime