Using Backbone.js with Barrister RPC
Motivation
I've recently been evaluating backbone.js for possible use on a new project. It's a very nice lightweight UI framework and has a built in way to persist model classes to the server using REST.
Last month I released a JSON-RPC implementation called Barrister RPC which provides an IDL grammar for defining structs and interfaces that your server code implements. One nice thing about JSON-RPC is that it's transport neutral, which means you can originate a call in your UI, send it over HTTP, then route it via some other transport (AMQP, ZMQ, Redis, etc) to the eventual implementation endpoint.
Thanks to Backbone's Backbone.sync
function, it's quite easy to use Barrister RPC as the backend for your
Backbone model classes.
Implementation
Going with the standard "To-Do" demo, let's say you have this Barrister IDL:
struct Todo {
id int [optional]
content string
order int
done bool
}
interface TodoService {
// returns Todo with given id
readTodo(id int) Todo
// creates new Todo and returns generated id
createTodo(todo Todo) int
// updates Todo and returns id
updateTodo(todo Todo) int
// deletes Todo. returns true if delete succeeded. if no Todo found, returns false
deleteTodo(todo Todo) bool
}
You should be able to use an existing Todo Backbone client implementation. For example, the ServiceStack example
The only modification is the addition of a barrister
property to the Model class:
window.Todo = Backbone.Model.extend({
// this is the only addition
barrister: { entity: "Todo", interface: "TodoService", endpoint: "/api/todo" },
// ... rest of model here..
});
Then add this generic JS code that overrides Backbone.sync
:
Backbone.Barrister = {
// Stores Barrister.Client objects
// key: endpoint name (string)
// val: Barrister.Client instance
clients: {},
// Initializes a Barrister client for the given endpoint and passes it to callback()
// If client for that endpoint already exists, callback is immediately invoked.
// Otherwise a new Client is created, its contract loaded, and the callback invoked
// after the contract loads.
initClient: function(endpoint, callback) {
var client;
if (!Backbone.Barrister.clients[endpoint]) {
client = Barrister.httpClient(endpoint);
Backbone.Barrister.clients[endpoint] = client;
return client.loadContract(function() {
return callback(client);
});
} else {
return callback(Backbone.Barrister.clients[endpoint]);
}
},
// Implements Backbone.sync
sync: function(method, model, options) {
var b, param;
if (model.barrister) {
b = model.barrister;
// read and delete calls pass in the ID to the call
// other operations pass the entire model as JSON
if (method === "read" || method === "delete") {
param = model[model.idAttribute];
} else {
param = model.toJSON();
}
// Construct a JSON-RPC method as expected by Barrister
// the interface name and entity are on the Model's 'barrister' property
//
// e.g. a 'create' for Todo becomes: TodoService.createTodo
//
method = b.interface + "." + method + b.entity;
// Make the Barrister RPC call. We're using the client.request
// function directly, which still provides IDL contract validation
return Backbone.Barrister.initClient(b.endpoint, function(client) {
return client.request(method, [param], function(err, result) {
if (err) {
return options.error(model, err);
} else {
return options.success(model, result);
}
});
});
} else {
return options.error(model, "sync: model does not have barrister property set");
}
}
};
// Bind our sync function, replacing the default Backbone.sync
Backbone.sync = Backbone.Barrister.sync;
How it works
This solution uses a naming convention for the Barrister interface functions based on the entity name
and the method passed into the sync
function by Backbone. Here's a summary of the mapping:
backbone.js Model function | backbone.js method | Example Barrister function IDL |
---|---|---|
model.save() | create or update (depending on whether the model's idAttribute is set) |
// Return types are up to you // backbone.js doesn't care (to my knowledge..) createFoo(f Foo) int updateFoo(f Foo) int |
model.fetch() | read | readFoo(id int) Foo |
model.destroy() | delete | deleteFoo(id int) bool |