History

A few months ago, inspired by the desire to build a new and optimized user experience, we decided our front-end was due for an overhaul. When users visit Zumper looking for a house or apartment for rent, they initially spend their time researching neighborhoods and cities. Once they’ve found the right location, they turn their attention to discovering the perfect building or unit. Slowly, they’re putting together their favorite listings, keeping tabs on specific areas, and eventually visiting and applying to the ones they really love. Keeping this in mind, we wanted to create a cohesive search tool which provides context to your search right where you need it. And with that, our new search tool, codenamed Dora, was born.

First version of our search tool
First version of our search tool

From a technological perspective, the first version of our consumer application used Django templates with jQuery and Closure for dynamic client-side rendering. When we built the first version of our Pro product, AngularJS became our framework of choice to provide a more MVC-centric solution to an application which contained few views but advanced functionality within those views. Over time, we used more services and directives across the front-end code base and opted for Angular when we built new parts of the site. We decided to adopt AngularJS fully as it provides a great foundation for MVC on the client side. We also rewrote all of our legacy code from scratch. After examining web applications built by Airbnb, Radius, and Virgin America, we decided to build some of our own custom solutions on the shoulders of giants.

Contextual Search

When we first rolled out our consumer product, we had two main verticals for our site: Map and Listing Feed (i.e. city and neighborhood pages). This allowed people to either take a visual top down approach to search or to view listings within a specific neighborhood or city. Initially, this was great for users who had different styles of search. Eventually, however, we found that the dichotomy caused the product to feel siloed and incohesive. Each view did not share the knowledge of a user’s search and there was no easy way to save searches.

Screen Shot 2015-02-05 at 1.49.10 PM
The Castro in our new search experience

As a result, we decided to unify the knowledge and share it between the two views. We now store the geographic data and filter parameters within a common service which allows you to easily jump between views without losing the context of the search. Furthermore, if you move the map when conducting a search on the list view (above), we will determine the city or neighborhood based on your current view. This allows you to easily see what makes your currently viewed neighborhood special (like the world class $6 burritos of The Mission I so very adore). You can also find our rent graphs that show you the median rent for the city or neighborhood and how it’s changed over the last few months. And if you decide to take a break from your search, when you come back, we’ll remind you of where you left off.

Descriptive URLs

One thing we noticed was that our users love to share listings and searches with others. When you’re looking for a new apartment or house, there are many scenarios where you’re in need of a second opinion. Maybe you’re moving to a city you’ve never been before. Perhaps you are looking for an apartment with friends who will soon become your roommates. Regardless of the scenario, it’s easier to do research with others when you’re all on the same page. For that reason, we decided to change the way we represent our URLs. It’s super simple to not only share a URL, but to understand and change the terms right from the address bar. For example:
Two bedroom apartments in San Francisco under $3,000
/apartments-for-rent/san-francisco-ca/2-beds/under-3000
Studio apartments in Chicago that allow pets
/apartments-for-rent/chicago-il/studios/pet-friendly

Apartments in Chelsea, New York which have at least two bedrooms and no fee
/apartments-for-rent/new-york-ny/chelsea/no-fee/2+beds

Want to see them all on a map? Just add /map after zumper.com and you’ll see the above searches on a full sized map.
/map/apartments-for-rent/new-york-ny/chelsea/no-fee/2+beds
We decided to use AngularUI Router to manage all the main states (and substates) of our client application. We then used our geo service to manage the geographic information and our filter service to determine the state of the filters. They would each listen to actions in the interface and maintain the state. But what about the URL? For all URL changes we wrote our own translator which could read changes to the state and write them to the URL (and vice versa).

Putting it all together

When I was working in the Mobile Display Ads division at Google, we built specialized libraries that were both obfuscated and compressed, making them small enough to travel quickly over the wire but sophisticated enough to provide rich media experiences across a wide array of devices. At Zumper, our users would be revisiting our site across the hours/days they conducted their apartment search. I wondered if there was a way we could partition the application on a view-by-view basis, constructing the full application based on each user’s path through her apartment search on Zumper.
Under the hood, our main concern became optimizing the serving of data with which to render our front-end views. Classic single-page AngularJS applications suffer from the necessity to require all modules at runtime, injecting and compiling the application (bootstrapping) and then potentially loading templates for controllers or directives. On first load, our core library is loaded by default to set up the main application framework and router which intercepts the requests. When a specific page is requested, the AngularJS module for that given page is loaded dynamically in its compressed/obfuscated form. Angular is told about the newly loaded module, compiling and injecting it into the application, and finally rendering the page. If the user navigates to another page, we determine if the module/dependencies for that section are loaded yet. If not, they’re dynamically loaded before the template is served. In this manner, as a user navigates to different parts of our site one is slowly putting together all the pieces of our client side application. We decided to use ocLazyLoad as a simple solution for lazy loading our compressed AngularJS modules.
On the other hand, when loading a page directly from the server and not through our client side application (i.e. cold load), we supply our main view templates and core directive templates inline. We also integrate pertinent data objects to reduce the need for subsequent asynchronous requests. This allows for users coming to us from other sources to get a quickly loaded landing page, whether it’s a specific listing or a list of units in San Francisco. Our templates and scripts are also cached, resulting in a speedy search experience during subsequent sessions.
As a result, our vision for having a continuous but dynamic search experience went hand-in-hand with a dynamically loading and caching application that is constructed during your search. You should see for yourself and let us know what you think.
Happy Searching!

Find your next place