JS in the Front

REST in the Back



@pamelafox



hint: use space bar for more goodness

Web 1.0

Web 2.0

Web 2.5

Coursera Admin

Server ➜ HTML




JS ➜ HTML





RequireJS

<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>Coursera.org</title>
  <link href="https://dt5zaw6a98blc.cloudfront.net/site-static/8adbcdf6598e55b320cbc6717c5be02677022c42/css/home.css" rel="stylesheet" type="text/css">
 </head>
 <body>
  <div id="origami">loading...</div>
  <script type="text/javascript" src="https://dt5zaw6a98blc.cloudfront.net/site-static/8adbcdf6598e55b320cbc6717c5be02677022c42/js/core/require.js"></script>
  <script type="text/javascript" data-baseurl="https://dt5zaw6a98blc.cloudfront.net/site-static/8adbcdf6598e55b320cbc6717c5be02677022c42/" data-version="8adbcdf6598e55b320cbc6717c5be02677022c42" data-timestamp='1351212102857' data-debug='0' id="_require">
(function(el) {
   require.config({
     enforceDefine: true,
     waitSeconds: 14,
     baseUrl: el.getAttribute("data-baseurl"),
     urlArgs: el.getAttribute("data-debug") == "1" ? "v=" + el.getAttribute("data-timestamp") : "",
     callback: function() {
       require(["js/routes/home"]); // bootup coursera
     }
   });
})(document.getElementById("_require"));
  </script>
 </body>
</html>

Backbone Router


var routes = {};
routes["/admin/:collectionLabel"] = function(collectionLabel) {
  var collectionClass = getCollectionForLabel(collectionLabel);
  if (!collectionClass) {
    Coursera.router.trigger("error", 403);
    return;
  }
  var regions = {};
  regions['body'] = {
      "pages/site-admin/views/CollectionAdminPageView": {
        collection: new collectionClass()
      }
    }
  }
  Coursera.region.open(region);
};

Backbone.history.start({pushState: true});
    

Backbone View


define([
  "jquery",
  "backbone",
  "js/core/coursera",
  "pages/site-admin/views/CollectionAdminListView.html"
  ],
function($, Backbone, Coursera, ListTemplate) {

  var view = Backbone.View.extend({
    initialize: function() {
      var self = this;
    },

    render: function() {
      var self = this;
      self.$el.html(ListTemplate({
        config: Coursera.config,
        collection: self.collection
      }));
      return this;
    }
  });
  return view;
});
    

JS ➜ Server ➜ JSON




Backbone & REST

Backbone Model


define(["jquery",
          "backbone",
          "js/core/coursera",
          "pages/site-admin/models/AdminModel"
          ],

function($, Backbone, Coursera, AdminModel) {

  var model = AdminModel.extend({
    defaults: {
      status: 0
    },

    url: 'admin/courses'
  });

  return model;
});
    

Backbone Collection


define(["backbone",
        "js/core/coursera",
        "pages/site-admin/collections/AdminCollection",
        "pages/site-admin/models/CourseAdminModel"
        ],
function(Backbone, Coursera, AdminCollection, CourseAdminModel) {

  var collection = AdminCollection.extend({

    url: 'admin/courses',
    
    model: CourseAdminModel,

    comparator: function(model) {
      return model.get('topic__name');
    }
    
  });

  return collection;
});
    

Fetching Data

var collection = new CourseCollection();
collection.bind('reset', displayCollection);
collection.fetch();
    
HTTP GET /api/courses
[{id: 12,
 title: 'Machine Learning Fall 2012',
 instructors: 'Andrew Ng',
 universities: [1],
 //...
}]
    

function displayCollection() {
  collection.each(function(model) {
    $el.append(model.get('title'));
  });
}
    

Saving Data

var model = new CourseAdminModel({
      title: 'Rapping 101',
      instructors: 'Pamela "The Fox" Fox'
    });
model.bind('change', displayModel);
model.save();
HTTP POST /api/courses
{title: 'Rapping 101', instructors: 'Pamela "The Fox" Fox'}
function displayModel() {
  $el.append('<a href="' + model.get('id') + '">' + model.get('title'));
}

What about bots?

What about bots?



Scala ➜ Play ➜ Selenium ➜ Chrome ➜ HTML



Nginx:

if ($http_user_agent ~ ^.*({{ bot_user_agents }}).*$) {
       proxy_pass https://jitr.coursera.org;
       break;
}

What about testing?

API:
def test_get_categories(self):
    client = Client()
    cats_url = reverse('api_categories')
    create_and_login_as_superuser(client)
    response = client.get(cats_url)
    response_json = simplejson.loads(response.content)
    self.assertTrue(len(response_json) > 0)
    fields = ['name', 'id', 'short_name', 'description']
    for field in fields:
        self.assertTrue(field in response_json[0])
Frontend:
def test_dashboard_display(self):
  self.login_as_superuser()
  self.open_admin()
  self.helper.wait_for_num_els('.model-admin-section', 6)
  self.helper.click('a[href="/admin/data/courses"]')
  self.helper.wait_for_title('All %s' % label)

But why?

Separation of Presentation and Data

Same Backend, Multiple Frontends

Same Backend, Multiple Clients

Backend Independence!

Internal ➜ External API

Rapping it Up...


Give me the Mic
Crockford and Eich
From Netscape to Node
It's been quite a hike

Onclick to Require was such a ride
With GitHub my skills have nowhere to hide
My lack of globals is a point of pride
I'm hitting my stride on the client-side

JS in the front, REST in the back
Ain't it time you changed your stack?
Server-side rendering? That's just whack
Once you go Backbone, you never go back
epic rap, yo