[update: keeping this around for posterity, but we ultimately went to a pub/sub pattern, using Amplify.js. Amplify isn't strictly necessary to implement pub/sub, but it has some nice features and syntax]
I have a REST API, which naturally you can just use HTTP calls (mainly Ajax) in order to send and receive data. I am building a layer of abstraction on top of the raw REST API for JavaScript Developers. I already use jQuery in the project, so I am using $.ajax()
. I feel like I have arrived at a good pattern, but I'm not absolutely certain.
var ta = ta || {};
$.ajaxSetup({
cache: false
});
ta.api = {
heartbeat: function(successCallback, errorCallback) {
var callType = "heartbeat";
// if no custom handlers are provided, wire up the default callbacks or blank callback methods
successCallback = ta.utils.setDefaultIfNeeded(successCallback, ta.callbacks[callType].success);
errorCallback = ta.utils.setDefaultIfNeeded(errorCallback, ta.callbacks[callType].error);
var url = '/rs/heartbeat';
$.ajax({
url: url,
success: function(data) {
successCallback(data);
},
error: function(xhr) {
errorCallback(xhr);
}
});
},
version: function(successCallback, errorCallback) {
var callType = "version";
// if no custom handlers are provided, wire up the default callbacks or blank callback methods
successCallback = ta.utils.setDefaultIfNeeded(successCallback, ta.callbacks[callType].success);
errorCallback = ta.utils.setDefaultIfNeeded(errorCallback, ta.callbacks[callType].error);
var url = '/rs/version';
$.ajax({
url: url,
success: function(data) {
successCallback(data);
},
error: function(xhr) {
errorCallback(xhr);
}
});
}
}
ta.utils = {
setDefaultIfNeeded: function (providedHandler, defaultHandler) {
var checkedHandler = providedHandler;
if (typeof (providedHandler !== "undefined")) {
if (typeof (providedHandler) !== "function") {
if (typeof (defaultHandler) === "function") {
checkedHandler = defaultHandler;
} else {
checkedHandler = function () { /* intentionally empty */ };
}
}
} else {
checkedHandler = function () { /* intentionally empty */ };
}
return checkedHandler;
}
};
So, the API can accept success and error handlers on a per-call basis. The setDefaultIfNeeded
will do a few pieces of logic, but one of those pieces is an optional phase of using handlers from a callback object. So the callback object is optional, but it's a handy place to store and organize callbacks:
ta.callbacks = {
heartbeat: {
success: function(data) { console.log(data) },
error: function(xhr) { console.log(xhr.statusText) }
},
version: {
success: function(data) { console.log(data) },
error: function(xhr) { console.log(xhr.statusText) }
}
}
Another addition I plan to make but haven't tested yet is to have the functions return the jqXhr. It's already being made, so why not return it? But I haven't tested that yet so I didn't want to provide sample code that might be broken.
So, there are now a few options for using the API, depending on your needs:
- Make calls to ta.api methods and supply success and error handlers as needed. I.e. you would define them in the methods that ultimately call the REST API.
- Make void calls to ta.api, but the success and error handlers are supplied in the ta.callbacks object.
- You can still continue to make calls to the REST service with whatever other arbitrary methods you choose.
- *planned - define a variable against ta.api which becomes a jqXhr. You then have full access to the XHR and can do with it as you please.
I have a suspicion that some of you will recommend deferred/promises API, and I am not against it. But if I'm being honest, I can't visualize a strong way to encapsulate the various moving parts and have them extremely readable for other devs.