In this article, I will first take a high-level look at modern frontend architectures: In a time where web apps easily surpass 1 MB of JavaScript, what should we try to achieve? Second, based on these considerations, I’m going to argue that Backbone.js should fully support the traditional HTTP URL scheme.

The ideal web site architecture

Today’s typical web site architectures can be placed between two extremes, one being traditional server-side logic, the other being JavaScript-only single-page apps. In between, there are hybrid approaches. Pamela Fox does a great job of describing these architectures and their pros and cons. She also introduces some key requirements from the user’s perspective: Usability, Linkability and Searchability/Shareability. In her presentation, she gives a quick overview of how the architectures perform. This outlines the current situation quite well.

How should a modern site work? There are several reasons why one should combine the best of all approaches: Server-side robustness with a client-side turbo-boost. In practice, we run into problems when trying to share logic between server and client. I think this is an engineering problem that can and will be solved in the future.

So what is the key to future architecture? I think it is Progressive Enhancement from soup to nuts. Progressive Enhancement is still useful and necessary. A typical site should be able to fulfill its basic purpose somehow even without JavaScript. A machine that speaks HTTP and HTML should be able to read a site. Of course, modern web sites aren’t about static content, but about user interactivity. But in most of the cases, there are resources with a static representation, either text, video or audio.

In order to achieve Searchability and also performance, content needs to be rendered on the server to some extent. Twitter learned this lesson the hard way in the “#NewTwitter” days, when they experimented with completely client-side architecture, but ultimately went back to serving traditional HTML pages for each initial request. Still, twitter.com is a huge JavaScript app. JavaScript operates on top of the initial DOM and then takes over to speed up subsequent actions. Hopefully, we’ll see this hybrid approach more and more in the future.

Rendering HTML on the server-side is considered valuable again. That’s because the traditional stack of HTTP, URL and HTML is simple, robust and proven. It can be incredibly fast. It works in every user agent; browsers, robots and proxies are treated uniformly. Users can bookmark, share and save the content easily.

Cool URLs are cool!

Used correctly, URLs are a great thing. Web development is centered around them: Cool URLs don’t change, URLs as UI, RESTful HTTP interfaces, hackability and so on. The concept of HTTP URLs dates back to 1994.

When “Ajax” appeared in 2005, people quickly realized that it’s necessary to reflect the application state in the URL. For a long time, JavaScript apps weren’t able to manipulate the full URL silently without triggering a server request. To achieve Linkability, therefore, many JavaScript apps are using “Hash URLs”. It’s safe to set the fragment part of the URL, so this became common practice. Most JavaScript libraries for single-page apps still rely on Hash URLs. Among others, Backbone.js uses Hash URLs per default in its routing implementation.

Today we know that Hash URLs aren’t the best solution. In 2011 there was a big discussion after Twitter and Google introduced Hash URLs and “Hash Bang URLs” in particular. Most people agreed that this was a bad hack. Fortunately, HTML5 History (history.pushState and the popstate event) makes it possible to manipulate the URL without leaving the single-page app. In general, Hash URLs should only be used as a fallback for older browsers.

If you use pushState, all URLs used on the client need to be recognized by the server as well. If a client sends a request such as GET /some/path HTTP/1.1, the server needs to respond with a page that at least starts the JavaScript app. In the end, making the server aware of the request path is a good thing. Instead of just responding with the code for the JavaScript app as a catch all, the server should better respond with useful content. In this case, traditional URLs enable Searchability and Shareability. Take for example an URL like this:

http://www.google.com/search?hl=en&ie=utf-8&q=pushState

These kinds of URLs are a well-established standard, widely supported, and can be handled on both the server and the client. So my conclusion is: Future JavaScript-heavy web sites may be “single page apps” because there’s only one initial HTML document per visit, but they still use traditional URLs.

Backbone.js and query strings

Backbone.js has a great History module that observes URL changes and allows to set the URL programatically. However, it doesn’t support traditional URLs completely. The query part (?hl=en&ie=utf-8&q=pushState), also known as query string, is ignored when routing. In this second part of the article, I’d like to discuss the ramifications of this missing feature.

Backbone treats /search?q=heaven and /search?q=hell as the same URL. This renders the query string useless. You can “push” URLs with different query strings, but if the user hits the back button, Backbone won’t consider this as a URL change, since it ignores the change in the query string.

Chaplin.js, an opinionated framework on top of Backbone.js, tries to work around this by parsing the query string into a Rails-like params hash. But it ultimately fails to support query strings because of Backbone.History’s limitation. Full disclosure: I’m a co-author of Chaplin.

The lack of query string support in Backbone is deliberate. The maintainer Jeremy Ashkenas decided against it. In several GitHub issues, he provides rationale:

From issue 891:

In the end, I think most Backbone apps should definitely not have query params in their app URLs — they’re a server-side URL convention that doesn’t have much useful place in client-side routing. So we shouldn’t be supporting them by default — but if you want this behavior, it should be easy enough for you to implement

From issue 2126:

Backbone shouldn’t be messing with the search params, as they don’t have a valid semantic meaning from the point of view of a Backbone app. If you want to use them (on a page that has a running backbone app), that’s totally fine …

In the most recent issue, Jeremy points out that this is not a browser compability issue:

From issue 2440:

wookiehangover: The thing that’s problematic about this (and why querystrings are ignored as of 0.9.9) is due to a handful of very weird but very real bugs with querystring processing and character encoding between browsers.

Nope. Not in the slightest ;)

The reason why querystrings are ignored by Backbone is because:

Querystrings only have a defined meaning on the server-side. The browser does not normally parse or otherwise handle them.

While querystrings are fine in the context of real URLs, querystrings are entirely invalid in the context of #fragment URLs. Most Backbone apps deal with fragment urls sooner or later — even if you’re using pushState for most of your users, IE folks will still have fragments. So querystrings can’t be used in a compatible way.

Better to leave them out of your Backbone app, and use nice URLs instead. If you must have them for the server side of the equation, that’s fine — Backbone will just ignore them and continue about its business.

Party like it’s 1994! *

I’d like to answer to these statements here. First of all, it’s great to hear that there are no major browser issues blocking full URL support. Jeremy argues against query strings on another level:

Querystrings only have a defined meaning on the server-side. The browser does not normally parse or otherwise handle them.

Honestly, I don’t understand this point. You can process a query string on the server, but you can do that on the client as well. There are cases where query strings are processed almost exclusively on the client, for example the infamous utm_ parameters for Google Analytics.

A URL is a URL. Wherever a URL appears, its parts have a defined meaning – there are Internet Standards which define the meaning. It doesn’t matter which software generates the URL and which processes it, a query string should have the same meaning.

While querystrings are fine in the context of real URLs, querystrings are entirely invalid in the context of #fragment URLs.

This assumes that Backbone apps use Hash URLs instead of pushState. Well, most of them do and that’s indeed a source of pain. But technically the query string ?foo=bar is entirely valid inside the fragment part of a URL.

A URL like http://dahl.example.org/#search?q=matilda may look weird, but it is completely in line with RFC 3986. With pushState, you don’t have to think about URLs in URLs. You can use URLs like http://dahl.example.org/search?q=matilda. This is the form of URLs that has been around since 1994, for good reasons.

… even if you’re using pushState for most of your users, IE folks will still have fragments. So querystrings can’t be used in a compatible way.

Well, they can be used in a compatible way. It’s technically possible to put path and query string into a fragment. It might violate the semantics of traditional URLs, but syntactically, it’s still a valid URL.

Better to leave them out of your Backbone app, and use nice URLs instead.

Jeremy argues that client-side apps should encode query params inside the path, like

http://dahl.example.org/#books/order=asc/sort=published/

That’s what he calls a “nice URL”. I beg to differ. In the spirit of 1994, why not stick to traditional URLs like:

http://dahl.example.org/books?order=asc&sort=published

I see no reason why JavaScript apps should invent new URL syntaxes. Today’s JavaScript apps are using pushState and properly accessible URLs. They should not and don’t have to differ from the URL conventions that have been used since the beginning of the web.

It’s an RFC-compliant URL, there are plenty of server and client implementations to parse the query params into a useful hash structure. In contrast, if you use URLs like

http://dahl.example.org/#books/order=asc/sort=published/

… you cannot use these implementations, but have to write your own “nice URL” parser instead.

If you must have them for the server side of the equation, that’s fine — Backbone will just ignore them and continue about its business.

If you’re building an app that has accessible documents, traditional URLs and query strings, most likely you need to process the query string on the server and on the client side. For such apps it’s not an option that the server understands them and Backbone ignores them.

My fellow Chaplin author Johannes Emerich pointed out another reason why Backbone should not limit the use of URLs:

In the end the point is that Backbone is said to be an unopinionated framework. But pushing for query params to be encoded as paths is anything but unopinionated or flexible.

There are many reasons why you would want to see those params on the server: Include some JSON data to be processed immediately on client-side app startup; render a full initial static document that contains all the data and only let the client-side app take over from there (for speed/SEO), etc.

In effect, this way of handling params in URLs is saying that Backbone is really only meant for completely client-side apps, and that you have to jump through extra hoops if you are going for a hybrid approach.

Of course, Chaplin and other code could monkey-patch Backbone in order to introduce query string suppport. But since Backbone claims to be “unopinionated”, it should just support traditional URLs instead of making query strings impossible to use. The ultimate decision for or against query strings should be the user’s, not the library’s.

In short, Backbone should support query strings because future-proof JavaScript apps are based on traditional URLs.

Thanks to Johannes Emerich (knuton) for feedback and input.

7 comments
  1. Maybe time to fork Backbone into a REAL unopinionated clone framework?

    Maybe together with lo-dash, as a better underscore.js clone.

  2. Nice & beautiful URLs have been around much earlier, IIRC; but: in earlier (“more dark”) days of the internet, most people just didn’t care :-)
    Google gave the push, finally, when they introduced the beauty/read-iness of URLs as qualityparameter for the search result.

  3. Eugene Mirotin July 18, 2013 at 12:08 pm

    How about https://github.com/jhudson8/backbone-query-parameters ?
    Used it once, works OK

  4. Encoding parameters in the path of a URL has been around since 1996 (just) – with Tim Berners-Lee’s Matrix Parameters: http://www.w3.org/DesignIssues/MatrixURIs.html

    http://dahl.example.org/books;order=asc;sort=published/

    This has the benefit of definitely being included by older proxies and caches (where GET parameters aren’t always).

  5. BTW using hashes for old browsers has several disadvantages:
    * a website will have 2 urls for the same location (with and without the ‘#’ symbol)
    * you’ll encourage people using old browsers

    So in my ajaxify (and better-ajaxify as well) I decided to trigger default link behaviour/form submission for browsers that do not support History API. It works well and solves all issues above.

  6. I must agree with your points, I don’t see why Backbone.js have this limitation.

    I was developing an app with a search that uses more than 8 parameters. I ended up creating 2 functions the first is called when only the main search keyword is used together with the other default settings. It was triggered by by search/keyword

    The other function was sending the parameters to the server the classic way as a post without updating the URL. Not elegant and I hated it but I couldn’t find other ways around.

  7. Hi Mathias,

    We are building a large e-commerce site using Chaplin.js on the frontend.
    We use google closure templates for the views.
    They are shared with a java backend. By doing so, the backend can pre-render a page spitting all the HTML + the model as json, then the client can pick-up from there, effectively re-constructing the models/views needed automatically.

    Interestingly, you posted this exactly when we started running into this issue of query string url’s as we started on the search feature.

    I completely agree with you about the fact that there are no reasons querystring should be avoided on the frontend. They are as usefull as on the backend.

    The url we have designed where going to be something along the lines of:

    http://www.domain.com/search/keyword/:keyword?filter1=value1&filter2=value2&filter3=value3

    At the moment I can’t seem to even find a way to route that type of url to a controller.
    If anything the only thing I manage to do is that if I create a route for /search/keyword/:keyword, a url which ends with a querystring get routed properly but the querystring is not present in the url and I don’t get any information of it in the route parameters in the controller.

    You say it would be possible to monkey patch those issues, how would be your approach our url schema given that we would need to:

    - always reflect the proper url (with the querystring in)
    - be able to somehow get the querystring info somewhere, presumably the controller to handle it

    Thanks a lot!

    Benoît.