Let Your Backbone Slide – Extending RESTful web services

Let Your Backbone Slide - Extending RESTful web services

Backbone.js provides a very flexible framework for building JavaScript applications that interact with web services. Models and Collections serve to represent easily represent data entities, with operations to create, read, update, and delete. This set of operations – summarized as ‘CRUD’ – is very common to see in web applications.

How, though, do you interact with web services that provide more than just CRUD functionality? RESTful web services can certainly offer more than just CRUD operations, and application requirements may dictate such operations, so the Backbone.js Models and Collections will have to be modified or extended accordingly.

In this article, I’ll go over an example of how to use Backbone.js to communicate with some simple web services that provide CRUD functionality, or ‘endpoints’ along with some non-CRUD endpoints. Additionally, I’ll share some ideas on how to keep these endpoints organized in the Backbone.js Models and Collections, which helps keep the code and application self-documenting.

A blog post is a great example of an entity that could have CRUD endpoints, as well as additional ‘verbs’; you create new blog posts, read from the server to provide them to a reader or editor, update them with edits, corrections, or progress, and perhaps delete them. All blog posts should start as drafts, so there’s a need to publish (and unpublish) them, which is where we start to deviate from the standard web services and need some additional functionality from Backbone.js.

Let’s go ahead and define our blog post:

var BlogPost = Backbone.Model.extend({...});

Voila! Now, our CRUD operations are provided by Backbone.js:

Create (HTTP POST)

var blogPost = new BlogPost();
blogPost.set("title", "My First Post");
blogPost.save();

Read (HTTP GET)

blogPost.fetch();

Update (HTTP PUT)

blogPost.set("content", "Lorem ipsum something something");
blogPost.save();

Delete (HTTP DELETE)

blogPost.destroy();

As an aside, most Backbone.js operations assume that a Model is part of a Collection, which is how it provides the URL of the corresponding RESTful web service. For the sake of brevity, I’ve omitted the Collection operations. If you’d like more detail, the Backbone.js documentation is absolutely outstanding, and even includes annotated source code for a deeper dive.

Now, let’s define the web service that will satisfy these requests. We’ll use ‘services’ as our root path parameter and ‘blogposts’ to represent our Collection of blog posts.

Create (HTTP POST)

/services/blogposts

Read (HTTP GET)

/services/blogposts/{id}

Update (HTTP PUT)

/services/blogposts/{id}

Delete (HTTP DELETE)

/services/blogposts/{id}

Referencing the Collection again, a GET request to /services/blogposts would return all of the blog posts. This Collection would give us access to the individual blog post Models and provide the URL for the model to interact with the web services.

As you can see, the requests have an almost identical URL, with the Create operation being the exception. With the blog post’s unique ID provided in the URL, the indication is that we’re operating on a unique entity now, a single Model – our blog post. It’s logical that any operation performed on a single blog post must have an ID in the URL, followed by the action to be performed.

Let’s use ‘publish’ and ‘unpublish’ as our verbs that describe the additional operations that we’re going to perform on our blog post.

Since we’re modifying (Updating) the model, the request should be defined as:

Update (HTTP PUT)

/services/blogposts/{id}/publish

Now, the issue becomes clear; Backbone.js doesn’t know about our custom endpoints. A Model’s ‘url’ property is provided by its Collection, but can be overridden to allow for the functionality we must provide. Let’s define a function that will allow us to access our publish and unpublish web services:

publish: function() {
 this.url = this.collection.url + "/" + this.id + '/publish';
 this.save();
}

In the above function, we reference the collection’s URL – ‘services/blogposts’ along with the model’s id. Our custom endpoint, ‘/publish’, is then appended to the URL.

This isn’t the cleanest way to achieve this functionality – we’re keeping text literals in functional code, and this pattern would have to be replicated for each custom endpoint. In a large application, that may be dozens or hundreds of times. Also, since we’ve overridden the model’s url field, any future requests would go to our custom endpoint.

Now is when the flexibility of Backbone.js really shines – we can customize the ‘url()’ function to work with our custom endpoints, which we’ll declare in a member object:

endpoints: {
 PUBLISH: '/publish',
 UNPUBLISH: '/unpublish'
}

We’ll now modify the url() function of the model to append an endpoint (if set), and add a getter and setter for the current endpoint variable, and reset the endpoint when the sync is finished:

var BlogPost = Backbone.Model.extend({
url: function() {
 var base =
 _.result(this, 'urlRoot') ||
 _.result(this.collection, 'url') ||
 urlError();
 if (this.isNew()) return base;
 // Add the current endpoint to the url provided
  return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id) + this.getCurrentEndpoint();
},
setCurrentEndpoint: function(endpoint) {
 this.currentEndpoint = endpoint;
},
getCurrentEndpoint: function() {>
 if (this.currentEndpoint) {
  return this.currentEndpoint;
 }
 return "";
},
sync: function(method, model, options) {
 // sync stuff here
 ...
 this.setCurrentEndpoint("");
}
...
});

Now our Model has a much simpler time accessing a custom endpoint:

publish: function() {
 this.currentEndpoint = this.endpoints.PUBLISH;
 this.save();
}

This pattern makes our Models and Collections much easier to contain, and since we’ve integrated our custom endpoint functionality in Backbone.js’ base Model (rather than our own), it’s available to every Model and Collection we create.

A comprehensive organization of web services can offer great ‘readability’ of an application; the API should tell a story. It may not always be efficient or effective to give every action required a CRUD endpoint; following a sensible progression in URL endpoints or path parameters will give those using the API a better idea of what to expect in return. Similarly, when the Backbone.js Models and Collections are implemented to match, their interactions with the web services are clear, both to a new developer and one that hasn’t seen the code in 6 months.

There are many ways to organize RESTful web services depending on the needs of the client, the server, and the application. It may not always be convenient or useful to create unique CRUD web services for every operation required for an application, so some ‘overloading’ of the models/entities to provide access to non-CRUD endpoints may be an elegant solution. With the flexibility provided by Backbone.js, it is entirely possible to modify the Models and Collections to interact with a set of RESTful web services, regardless of the architecture.