Ruby on Rails monolith⟶Client-rendered React SPA on a v2 API
Swapping one small template for a React component broke unrelated parts of Airbnb's search page — and even its API response. The hunt for why ended in a rearchitecture away from Rails-delivered pages.
A Rails presenter had been mutating the model behind it for years, even when nobody was looking at the page.
Ten years of Rails delivering the highest-traffic page
Airbnb sees more than 75 million searches a day, which makes search the highest-traffic page on the site. For nearly ten years, engineers had evolved, enhanced and optimized the way Rails delivered that page.
The structure was page-by-page: landing page, then search results, then a single listing, then the booking flow — each one delivered standalone by Rails. With Experiences and Places arriving alongside Homes, the team wanted search to feel fluid instead of a sequence of full page loads. To get there, they had to leave the page-by-page model behind.
SYMPTOM
The component that broke the API
The search page contained some very old code. Early in the work, an engineer replaced a small Handlebars template — backed by a Rails presenter — with a simple React component. A local, contained, boring change.
Things immediately started breaking in entirely separate parts of the page. Then in the API response. A view-layer swap had reached out and corrupted data that had nothing to do with the thing on screen.
CLUES
Data that came from nowhere
The same shape of strangeness showed up elsewhere. Airbnb had magical Rails functions written long ago — add_bootstrap_data(key, value) could be called from anywhere in Rails to make data globally available on the client via BootstrapData.get(key). A helpful utility for a small team had become, in the team’s phrase, a source of untraceable witchcraft for a large one, because each team owned a different page and had cultivated its own mechanism for loading config.
Server-rendering made it worse. Airbnb uses Hypernova to server-render React, and before the refactor it was genuinely unclear whether an experiment assignment would blow up during server rendering, or whether a string translation available on the client would reliably exist on the server. If server and client output don’t match to the bit, the page flashes the difference and then re-renders entirely after load.
REVEAL
The presenter was writing, not reading
The reason a template swap could poison an API response: the presenter behind that Handlebars template was mutating the backing Rails model. It had been doing so for years, quietly shaping all downstream data — even when the UI wasn’t being rendered at all. Remove the presenter, and every consumer that had unknowingly been depending on its side effects changed behavior at once.
RESOLUTION
Every component eats API data, and only API data
Once the page rendered client-side, data had to be requested dynamically in a predetermined shape, so the team aligned on a “v2” of the API and made every component consume that canonical shape. GraphQL wasn’t an option when this refactor took place. The mechanical part was easy — step through every place Rails renders a React component and make sure the inputs are API shapes, then validate compliance with PropTypes on the client. The hard part was human: reeducating Business Travel, Growth, Vacation Rentals, the China and India market teams, Disaster Recovery and more that even though it was technically possible to pass data straight into a component, all data goes through the API.
For everything that isn’t API data — config, experiment assignment, translations, localization — they converged on one canonical bootstrap mechanism, a higher-order component that takes a single plain object and initializes the supporting tooling identically for server and client rendering. add_bootstrap_data was eliminated in a single shot, and engineers could no longer pass arbitrary keys into top-level React components.
With Rails no longer propping up the page, routes are navigated client-side with code-split, lazy-loaded bundles: server-render the page, ship the bare minimum JavaScript to make it interactive, then fetch the rest while the browser is idle. Transitions between routes came out about 5x faster.
A view layer that is allowed to write is not a view layer — it is an undocumented, unversioned part of your data model, and you will only find out how much of one on the day you try to delete it.
A plain-language, AI-drafted and human-edited retelling of the article published on web.archive.org,
reorganized and explained in our own structure and words, with original analysis in the editor's
note above. The facts, numbers, and decisions belong to the original author and are not altered.
For the full depth, read the source.