Let's Make a Framework: Ajax Improvements

2011-09-15 00:00:00 +0100 by Alex R. Young
*Let's Make a Framework* is an ongoing series about building a JavaScript framework from the ground up. These articles are tagged with [lmaf](http://dailyjs.com/tags.html#lmaf). The project we're creating is called [Turing](http://github.com/alexyoung/turing.js). Documentation is available at [turingjs.com](http://turingjs.com/).

API Redesign

As I mentioned last week, I thought it would be interesting to change
the turing.net module to have a chained API that looks more
like TJ Holowaychuk's
Superagent library. Part of the reason for doing this is to again demonstrate how easy it is to make
a simple and consistent chained API with JavaScript. However, TJ's
reasoning behind Superagent's design is worth thinking about, because he
makes some good points about the inconsistencies found in many modern

jQuery's API generally looks like this:

$.get('/user/1', function(data, textStatus, xhr) {
  // Callback manages result

TJ argued that reducing the arity in the callback would make it more

request.get('/user/1', function(res) {
  // The abstracted `res` object contains status code, result data, etc.

The res object neatly encapsulates everything you need to
deal with the server's response.

Another issue is any configuration requires falling back to the
full-blown \$.ajax() method. And, much like my initial
Turing design, we end up with calls like this:

  url: '/api/pet',
  type: 'POST',
  data: { name: 'Manny', species: 'cat' },
  headers: { 'X-API-Key': 'foobar' }
}).success(function(res) {

}).error(function() {


Whenever you're designing a JavaScript API and you find yourself
creating large configuration options, it's often worth sketching out
what a chained API would look like. I agree with TJ's example that this
looks better:

  .data({ name: 'Manny', species: 'cat' })
  .set('X-API-Key', 'foobar')
  .set('Accept', 'application/json')
  .end(function(res) {


Rather than providing two callbacks, users of the library can process
res however they wish. They don't depend on the framework's
interpretation of HTTP status codes.


To convert Turing's API to a Superagent-inspired chained API, I started
by writing tests based on my idealised API:

  .data({ key: 'value' })
  .end(function(res) {
    assert.equal('value', res.responseText);

Compare that to the old style:

$t.post('/post-test', {
  postBody: { key: 'value' },
  success: function(r) {
    assert.equal('value', r.responseText);
  error: function() {


The network methods call and return an internal method,
ajax. This was returning an object for chaining
then (the promise API), so I extended it:

function ajax(url, options) {
  var chain = {};

  // All the old ajax stuff goes here

  chain = {
    set: function(key, value) {
      options.headers[key] = value;
      return chain;

    send: function(data, callback) {
      options.postBody = net.serialize(data);
      options.callback = callback;
      return chain;

    end: function(callback) {
      options.callback = callback;
      return chain;

    data: function(data) {
      options.postBody = net.serialize(data);
      return chain;

    then: function() {
      if (promise) promise.then.apply(promise, arguments);
      return chain;

  return chain;

As with all chained APIs, the trick is to just return an object with the
expected methods. Here I return the same object from each method. The
only other things I had to change were to make
respondToReadyState always call
options.callback if present (else it looks for
success/error callbacks from the promise API or old style API), and I
made net.serialize cope with strings.


As we've seen before in this series, implementing chainable APIs is
pretty easy. In this case, a chainable network API seems a lot cleaner
than large configuration objects

These changes can be reviewed in commit