Introduction
JavaScript applications have evolved dramatically over the past few years. What started as simple form validation scripts have transformed into complex single-page applications that rival desktop software in functionality. However, as JavaScript applications grow larger, they become increasingly difficult to maintain. Code becomes tangled, jQuery selectors proliferate, and making changes to one part of the application unexpectedly breaks another.
This is where Backbone.js comes in. Created by Jeremy Ashkenas (the same developer behind CoffeeScript and Underscore.js), Backbone provides structure to JavaScript applications by organizing code into Models, Views, Collections, and Routers. It's not a full-featured framework like some of the heavier options out there—instead, it provides the minimal structure needed to keep your code organized while staying out of your way.
I've been using Backbone for several months now on various projects, and I'm impressed by how it strikes a balance between structure and flexibility. It doesn't prescribe exactly how you should build your application, but it provides proven patterns that scale well as your codebase grows.
What is Backbone.js?
Backbone.js is a lightweight JavaScript library that provides structure for web applications. Released in October 2010 and actively developed since, Backbone has gained significant traction in the JavaScript community. Major applications like Trello, Foursquare, and many others are built with Backbone.
At its core, Backbone implements the MVC (Model-View-Controller) pattern, though it's more accurately described as MV* since it doesn't have traditional Controllers. Instead, it uses Routers for URL management and Views that handle both display logic and user interaction.
The library is remarkably small—only about 1,000 lines of code and around 6KB when minified and gzipped. Despite its size, it provides everything you need to build sophisticated single-page applications.
Backbone has only two hard dependencies:
- Underscore.js: A utility library providing functional programming helpers
- jQuery (or Zepto): For DOM manipulation and Ajax requests
Why Use Backbone.js?
Organization and Structure
The biggest benefit of Backbone is organization. Without a framework, JavaScript applications tend to devolve into a mess of global variables, event handlers scattered throughout the code, and DOM elements that serve as the de facto data store. Backbone separates concerns: data lives in Models and Collections, display logic lives in Views, and URL routing lives in Routers.
This separation makes your code easier to understand, test, and maintain. When you need to change how data is fetched, you look at the Model. When you need to change how something is displayed, you look at the View. This clarity becomes invaluable as applications grow.
Minimal and Flexible
Unlike some JavaScript frameworks that enforce specific ways of doing everything, Backbone is deliberately minimal. It provides the core abstractions you need but doesn't dictate the rest of your architecture. You can use your preferred templating engine, Ajax library, or DOM manipulation approach. This flexibility means Backbone integrates easily with existing code and doesn't require rewriting your entire application.
RESTful by Default
Backbone is designed to work seamlessly with RESTful APIs. Its Models and Collections communicate with the server using standard HTTP methods (GET, POST, PUT, DELETE) and expect JSON responses. If your backend follows REST conventions, Backbone works almost magically with minimal configuration.
Event-Driven Architecture
Backbone uses events extensively. Models trigger events when they change, Views can listen for those events and update automatically, and custom events let different parts of your application communicate without tight coupling. This event-driven approach makes it easy to keep your UI synchronized with your data.
Learning Curve
Compared to larger frameworks, Backbone has a gentle learning curve. If you understand JavaScript objects, prototype inheritance, and events, you can be productive with Backbone quickly. The entire API is small enough to hold in your head after a few days of use.
Core Concepts
Let's explore Backbone's main components: Models, Collections, Views, and Routers.
Models
Models represent your data. A Backbone Model is a JavaScript object with methods for getting, setting, validating, and persisting data. Models also trigger events when data changes, allowing Views to update automatically.
Here's a simple example:
// Define a model
var Book = Backbone.Model.extend({
defaults: {
title: '',
author: '',
published: null,
pages: 0
},
validate: function(attrs) {
if (!attrs.title) {
return "Title is required";
}
if (attrs.pages < 0) {
return "Pages must be positive";
}
}
});
// Create an instance
var book = new Book({
title: "JavaScript: The Good Parts",
author: "Douglas Crockford",
published: 2008,
pages: 176
});
// Get attributes
console.log(book.get('title')); // "JavaScript: The Good Parts"
// Set attributes (triggers 'change' event)
book.set({ pages: 180 });
// Listen for changes
book.on('change:pages', function() {
console.log('Page count changed to ' + book.get('pages'));
});
Models can be saved to and fetched from a server with simple method calls:
// Save to server (POST or PUT request)
book.save();
// Fetch from server (GET request)
book.fetch();
// Delete from server (DELETE request)
book.destroy();
Backbone constructs the appropriate URLs automatically based on the model's urlRoot or its Collection's url.
Collections
Collections are ordered sets of Models. They provide methods for adding, removing, finding, and iterating over Models. Collections can also sync with the server to fetch multiple records at once.
// Define a collection
var Library = Backbone.Collection.extend({
model: Book,
url: '/api/books'
});
// Create an instance
var library = new Library();
// Fetch books from server
library.fetch();
// Add a book
library.add(book);
// Find books
var crockfordBooks = library.where({ author: "Douglas Crockford" });
// Iterate over books
library.each(function(book) {
console.log(book.get('title'));
});
// Listen for changes
library.on('add', function(book) {
console.log('Added: ' + book.get('title'));
});
Collections include dozens of Underscore.js methods like filter, map, reduce, sortBy, and more, making it easy to work with groups of models.
Views
Views handle displaying data and responding to user interaction. In Backbone, Views aren't templates—they're JavaScript objects that manage a portion of the DOM. Views typically render templates using a templating engine like Underscore's templates, Mustache, or Handlebars.
var BookView = Backbone.View.extend({
tagName: 'li',
className: 'book',
template: _.template(
'<h3><%= title %></h3>' +
'<p>by <%= author %></p>' +
'<p><%= pages %> pages</p>' +
'<button class="remove">Remove</button>'
),
events: {
'click .remove': 'removeBook'
},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
removeBook: function() {
this.model.destroy();
}
});
// Create and render view
var bookView = new BookView({ model: book });
$('#book-list').append(bookView.render().el);
This View demonstrates several important concepts:
- template: A function that generates HTML from data
- events: A declarative way to bind DOM events to View methods
- listenTo: Automatically listen to model events and call methods
- render: Generate HTML and update the DOM
- this.$el: A cached jQuery reference to the View's element
The View automatically updates when the model changes because we listen to the change event. When the model is destroyed, the View removes itself from the DOM.
Routers
Routers connect URLs to application state. They enable bookmarkable, shareable URLs and browser back/forward button support in single-page applications.
var AppRouter = Backbone.Router.extend({
routes: {
'': 'home',
'books': 'listBooks',
'books/:id': 'showBook',
'books/:id/edit': 'editBook',
'*path': 'notFound'
},
home: function() {
console.log('Home page');
},
listBooks: function() {
console.log('Show all books');
library.fetch();
},
showBook: function(id) {
console.log('Show book ' + id);
var book = new Book({ id: id });
book.fetch();
},
editBook: function(id) {
console.log('Edit book ' + id);
},
notFound: function(path) {
console.log('Not found: ' + path);
}
});
// Start the router
var router = new AppRouter();
Backbone.history.start();
Routes are defined as a hash mapping URL patterns to methods. Parameters in URLs (like :id) are passed as arguments to the corresponding method. The wildcard pattern *path matches anything that doesn't match other routes.
Backbone uses pushState when available, giving you clean URLs without hash fragments. For older browsers, it falls back to hash-based URLs automatically.
Building a Complete Example
Let's build a simple task management application to see how all these pieces fit together.
The Model and Collection
// Task model
var Task = Backbone.Model.extend({
defaults: {
title: '',
completed: false
},
toggle: function() {
this.save({ completed: !this.get('completed') });
}
});
// Task collection
var TaskList = Backbone.Collection.extend({
model: Task,
url: '/api/tasks',
completed: function() {
return this.where({ completed: true });
},
remaining: function() {
return this.where({ completed: false });
}
});
var tasks = new TaskList();
The Views
// View for individual task
var TaskView = Backbone.View.extend({
tagName: 'li',
className: 'task',
template: _.template(
'<input type="checkbox" class="toggle" <%= completed ? "checked" : "" %> />' +
'<span class="title"><%= title %></span>' +
'<button class="delete">Delete</button>'
),
events: {
'click .toggle': 'toggleTask',
'click .delete': 'deleteTask'
},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('completed', this.model.get('completed'));
return this;
},
toggleTask: function() {
this.model.toggle();
},
deleteTask: function() {
this.model.destroy();
}
});
// View for the entire application
var AppView = Backbone.View.extend({
el: '#app',
template: _.template(
'<input id="new-task" type="text" placeholder="What needs to be done?" />' +
'<ul id="task-list"></ul>' +
'<div id="stats">' +
'<span id="remaining"></span> remaining' +
'</div>'
),
events: {
'keypress #new-task': 'createTask'
},
initialize: function() {
this.listenTo(tasks, 'add', this.addTask);
this.listenTo(tasks, 'reset', this.addAll);
this.listenTo(tasks, 'all', this.updateStats);
this.render();
tasks.fetch();
},
render: function() {
this.$el.html(this.template());
this.taskList = this.$('#task-list');
return this;
},
createTask: function(e) {
if (e.which !== 13) return; // Enter key
var input = this.$('#new-task');
if (!input.val().trim()) return;
tasks.create({
title: input.val().trim()
});
input.val('');
},
addTask: function(task) {
var view = new TaskView({ model: task });
this.taskList.append(view.render().el);
},
addAll: function() {
this.taskList.empty();
tasks.each(this.addTask, this);
},
updateStats: function() {
var remaining = tasks.remaining().length;
this.$('#remaining').text(remaining);
}
});
// Start the app
var app = new AppView();
This example demonstrates how Backbone applications are structured:
- Models represent individual data items (tasks)
- Collections manage groups of models (the task list)
- Views render models and handle user interaction
- Views listen to model and collection events to stay synchronized
- The app view coordinates everything
With this structure, adding features is straightforward. Want to filter tasks? Add methods to the Collection. Want to edit tasks inline? Add events and methods to the TaskView. Want different URLs for different filters? Add a Router.
Working with Templates
Backbone doesn't include a templating engine—you bring your own. The most common choices are:
Underscore Templates
Underscore.js (Backbone's dependency) includes a simple templating system:
var template = _.template(
'<h2><%= title %></h2>' +
'<p><%= description %></p>'
);
var html = template({
title: 'My Title',
description: 'My Description'
});
Mustache or Handlebars
For logic-less templates, use Mustache or Handlebars:
var template = Handlebars.compile(
'<h2>{{title}}</h2>' +
'<p>{{description}}</p>'
);
var html = template({
title: 'My Title',
description: 'My Description'
});
External Templates
For larger templates, keep them in separate files or script tags:
<script type="text/template" id="book-template">
<h3><%= title %></h3>
<p>by <%= author %></p>
</script>
var BookView = Backbone.View.extend({
template: _.template($('#book-template').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
Best Practices
Keep Views Small
Each View should manage a specific portion of the UI. Break complex interfaces into multiple Views, each with a clear responsibility. This makes Views easier to test and reuse.
Use Custom Events
For communication between different parts of your application, use custom events on a global event dispatcher:
var dispatcher = _.extend({}, Backbone.Events);
// Trigger events
dispatcher.trigger('book:selected', book);
// Listen for events
dispatcher.on('book:selected', function(book) {
console.log('Selected: ' + book.get('title'));
});
Avoid Memory Leaks
When removing Views, clean up event listeners:
var BookView = Backbone.View.extend({
close: function() {
this.stopListening();
this.remove();
}
});
Or use listenTo instead of on—Backbone cleans up listenTo listeners automatically when you call remove().
Validate on the Server Too
While client-side validation provides immediate feedback, always validate on the server. Client-side validation can be bypassed, so server-side validation is essential for security.
Use render Conventions
Make render return this so you can chain calls:
$('#container').append(view.render().el);
This is a Backbone convention that makes code more concise.
Integration with Existing Code
Backbone works well with existing applications. You don't need to rewrite everything—you can introduce Backbone incrementally:
- Start with one feature as a Backbone module
- Have it communicate with existing code via custom events
- Gradually move more functionality into Backbone as appropriate
This gradual approach reduces risk and lets you learn Backbone in the context of real problems.
Testing Backbone Applications
Backbone's structure makes testing straightforward. Models, Collections, and Views are regular JavaScript objects that can be instantiated and tested in isolation.
For unit testing, popular tools include:
- Jasmine: BDD-style testing framework
- QUnit: jQuery's testing framework
- Mocha: Flexible testing framework
Example test with Jasmine:
describe('Task Model', function() {
it('should have default values', function() {
var task = new Task();
expect(task.get('title')).toBe('');
expect(task.get('completed')).toBe(false);
});
it('should toggle completed state', function() {
var task = new Task({ completed: false });
task.toggle();
expect(task.get('completed')).toBe(true);
});
});
Performance Considerations
Batch DOM Updates
When rendering many items, batch DOM operations to avoid repeated reflows:
var fragment = document.createDocumentFragment();
collection.each(function(model) {
var view = new ItemView({ model: model });
fragment.appendChild(view.render().el);
});
container.appendChild(fragment);
Use Efficient Selectors
Cache jQuery selectors in initialize to avoid repeated DOM queries:
initialize: function() {
this.input = this.$('#new-task');
this.list = this.$('#task-list');
}
Throttle Expensive Operations
For operations triggered frequently (like scroll or resize), use Underscore's throttle or debounce:
events: {
'scroll': 'handleScroll'
},
initialize: function() {
this.handleScroll = _.throttle(this.handleScroll, 100);
}
Comparing Backbone to Alternatives
As JavaScript frameworks proliferate, you might wonder how Backbone compares to alternatives.
Compared to jQuery Alone
jQuery is excellent for DOM manipulation and Ajax, but it doesn't provide application structure. As applications grow, jQuery-only code becomes difficult to maintain. Backbone builds on jQuery (or Zepto) but adds the organizational patterns needed for larger applications.
You still use jQuery for DOM manipulation, but Backbone ensures your code follows consistent patterns rather than evolving into spaghetti code.
Compared to Heavier Frameworks
Some JavaScript frameworks provide more features than Backbone: two-way data binding, comprehensive widget libraries, strict application architecture. These frameworks make more decisions for you, which can be helpful for large teams or complex applications.
Backbone takes the opposite approach: provide minimal structure and let developers make their own choices. This means more flexibility and a smaller learning curve, but also more decisions to make. For many applications, Backbone's lightweight approach is exactly right.
When to Use Backbone
Backbone is ideal when you:
- Want structure without excessive framework overhead
- Are building a single-page application
- Have a RESTful backend that returns JSON
- Need flexibility in your architecture
- Want to integrate with existing code gradually
Backbone might not be the best choice if you need extensive IE6/7 support, want a complete UI widget library included, or prefer frameworks that make all architectural decisions for you.
Learning Resources
The Backbone ecosystem is growing rapidly. Helpful resources include:
- Official Documentation: Clear, concise API documentation with examples at documentcloud.github.com/backbone
- Backbone Fundamentals: Free online book by Addy Osmani covering patterns and best practices
- Backbone Tutorials: Growing collection of community tutorials on various blogs
- Source Code: At only 1,000 lines, reading the annotated source code is educational and surprisingly approachable
- Backbone Google Group: Active community where you can ask questions and share knowledge
- GitHub Repository: Watch the repository to see how the framework evolves
Many developers also find it helpful to study open-source Backbone applications. Seeing how real applications structure their code provides practical insights beyond what documentation can offer.
Key Takeaways
Backbone.js provides just enough structure to organize JavaScript applications without being prescriptive about how you build them. Its small size and minimal API make it approachable, while its flexibility allows it to scale from small widgets to large single-page applications.
The MVC-inspired architecture—Models for data, Collections for groups, Views for display, Routers for URLs—creates a clear separation of concerns that makes code easier to understand and maintain. The event-driven approach keeps components loosely coupled while staying synchronized.
Backbone isn't a complete solution—it's a foundation. You'll still need to make decisions about templating, validation, authentication, and many other concerns. But that's by design. Backbone gives you structure without constraining your choices.
If you're building a JavaScript application with more than a few hundred lines of code, you should seriously consider Backbone. The initial investment in learning the library pays dividends as your application grows. Your code will be more organized, more testable, and more maintainable.
Try building a small application with Backbone. Start with Models and Collections to organize your data, add Views to display it, and introduce a Router if you need URL management. I think you'll find, as I have, that Backbone makes JavaScript development more enjoyable and sustainable at scale.