Server-Side Pagination in Backbone.js: Replacing Collections with Core APIs
Learn how to paginate a Backbone.Collection on the server using only core APIs: parse metadata, fetch single pages with reset, and prevent stale responses with request tokens.
02 Aug 2025, 20:24 UTC

The Problem: Collections That Grow Without Bound
When a Backbone.Collection fetches from a paginated endpoint without special handling, every page appends to the same array. A list that starts at 10 records becomes 1,000 after 100 page clicks, and the browser re-renders all of them. The useful takeaway is that server-side pagination in Backbone core means replacing the collection contents on each fetch, not appending to them, and keeping the server's total count outside the model array.
Desired Outcome
A single view displays only the current page of models. The collection derives total pages from server metadata rather than counting local models. Next and previous controls live in the view and trigger a new fetch when clicked. The collection never accumulates pages in memory unless the product explicitly requires client-side filtering across every record.
Prerequisites
- Load the Backbone version already pinned in the project along with its required Underscore and jQuery dependencies for that release. Collection events and fetch options have shifted across Backbone versions, so verify behavior against the source bundled with your version.
- Define either
Model.urlRootorCollection.urlsoBackbone.synccan issue REST requests. - Confirm the server returns a stable
idfor every record. Backbone uses ids for model identity; without them,resetandsetcannot reliably match models across pages.
Focused Procedure
Parse Metadata Without Polluting the Model Array
Override Collection.parse so it returns only the records array and stashes pagination metadata on the collection instance. Returning the whole response object breaks collection methods because Backbone expects an array from parse.
var PaginatedCollection = Backbone.Collection.extend({
url: '/api/items',
parse: function(response) {
this.meta = {
total: response.total,
page: response.page,
per_page: response.per_page
};
return response.records;
}
});
Run this code in your browser or application bundle where Backbone is already loaded. No special permissions are required. Replace /api/items with your actual endpoint.
Fetch a Single Page and Replace the Collection
Call fetch with data for query parameters and reset: true so the collection is emptied and repopulated. This produces a single reset event that views can use to re-render.
collection.fetch({
data: { page: 2, per_page: 10 },
reset: true
});
Expected check: after the request completes, collection.length equals per_page (10) except on the final page. The first model's id matches the first record returned by the server.
Wire the View to Collection Events
Keep page controls in the view, not in the model. Listen to sync or reset to render the page, and listen to request to show a loading state. A loadPage function should validate the requested page against collection.meta.total before fetching.
var PaginatedView = Backbone.View.extend({
events: {
'click .next': 'goNext',
'click .prev': 'goPrev'
},
initialize: function() {
this.listenTo(this.collection, 'request', this.showLoading);
this.listenTo(this.collection, 'sync reset', this.render);
this.listenTo(this.collection, 'error', this.showError);
},
loadPage: function(page) {
var perPage = this.collection.meta ? this.collection.meta.per_page : 10;
var totalPages = Math.ceil(this.collection.meta.total / perPage);
if (page < 1 || page > totalPages) { return; }
this.collection.fetch({ data: { page: page, per_page: perPage }, reset: true });
},
goNext: function() {
this.loadPage(this.collection.meta.page + 1);
},
goPrev: function() {
this.loadPage(this.collection.meta.page - 1);
},
render: function() {
// render this.collection.models and page controls
}
});
Expected Checks
- After fetch,
collection.lengthequalsper_pageexcept on the final page. collection.metamatches the server response metadata.- The first model
idmatches the first returned record. - The browser Network tab shows exactly one request per page change, containing the expected
pageandper_pagequery parameters.
Recovery Options for Failed or Stale Fetches
Keep the Previous Page on Error
Handle the collection error event by keeping the previous page visible and showing a retry affordance. Do not clear the collection unless the fetch succeeded.
this.listenTo(this.collection, 'error', function() {
this.hideLoading();
this.showRetry();
});
Prevent Out-of-Order Responses
Concurrent fetches can resolve out of order. Without a guard, an older slow response can overwrite a newer page. Use a monotonically increasing request token and ignore responses that do not match the latest token.
var requestToken = 0;
loadPage: function(page) {
var token = ++requestToken;
this.collection.fetch({
data: { page: page, per_page: 10 },
reset: true,
success: function(collection, resp) {
if (token !== requestToken) { return; }
// safe to render
}
});
}
Decide Empty-Page Behavior
If a page returns zero records, either render an empty state or redirect to the last valid page. Normalize page and per_page on both server and client so page=0, negative values, or oversized per_page cannot produce inconsistent totals.
Testing Approach
Stub Backbone.sync or use a fixture server to assert parse behavior, request payloads, event order, and error handling. Include a case where the last page has fewer records than per_page and a case where the server returns HTTP 500.
To verify locally, run a fixture with 25 records and per_page=10. Assert page 1 and 2 have length 10, page 3 has length 5, and meta.total remains 25. Add temporary logs in parse, request, sync, reset, and error to verify event order and that stale responses are ignored.
Limitations and Practical Verification
This approach uses Backbone core only. Do not assume pageable collection behavior from plugins is part of Backbone core; implement pagination with Collection, fetch, parse, and view events unless a plugin is already a declared dependency.
Starting Backbone.history can trigger route handlers that fetch immediately. Guard against duplicate initial page loads when page state is also encoded in the URL.
To check the result, open the browser Network tab, click through pages, and confirm each click sends exactly one request with the correct query parameters and no extra fetch occurs after Backbone.history.start(). Force an HTTP error from a stub or local server and confirm the previous page remains rendered while the error/retry path runs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.