Moving from Rails to Node
What do you do when you have a large, monolithic Rails app with technical debt creating performance and maintainability problems?
You convert it to a NodeJS app. At least, that’s what we were planning on doing at Goodsmiths before its untimely demise.
Current Tech Stack
In 2013, the goodsmiths.com e-commerce website comprised a Rails app, plus some Rails/Sinatra apps for backend APIs and processes. It used the traditional setup with Haml, Sass, and jQuery all inside Rails and combined using the asset pipeline.
(In these illustrations, the browser/client is at the top, and the server-side tech is at the bottom. RE, AS, and GE are the different API services.)
As the website grew, though, we were running into multiple problems:
- Performance was suffering. The average time to serve a webpage and its components was close to 10 seconds.
- More of the code was moving to front-end technologies, with rich experiences and complex interactions occurring without a full page request from the server. The thick stateful architecture was not conducive to building this type of experience.
- I was constantly running into issues with Rails. The Ruby ecosystem has a lot of poorly written, poorly maintained, and slow software.
The initial idea was to use the standard SPA “thin server” architecture to move most of the rendering and workflow into the front-end (using a framework like Backbone or Ember). The server would only be used for the APIs.
The biggest concern with this approach is search engine and accessibility. For a website that has a broad customer base and needs good SEO, the content and functionality must be accessible. This can be fixed by either providing a barebones (ugly) website that still has the functionality, or using a “headless” proxy like PhantomJS.
After doing some research, and inspired by a LinkedIn Engineering video (that I can’t find anymore, sorry!) and Twitter’s “thin client” architecture, we decided to take a hybrid approach. The API(s) would still be separated, but rather than have the routing in the front-end, a “UI layer” back-end would handle the UI workflows and state.
The initial page load would consist of minimal HTML and JS to bootstrap the main app. Each page state would query the UI layer for the appropriate template and data, which would be rendered client-side. For even better performance, the initial page content could be pulled from the UI server, with subsequent pages using Ajax.
This would also fit better with our 2 main user bases: normal JS-enabled users, and search engines and non-JS users.
The advantages over a fat client:
- Faster time for initial page loading and rendering.
- Good SEO/accessibility is easier to accomplish because JS is not required to retrieve the state and content (except for a simple PhantomJS proxy, or a server alternative, to render the templates).
- A single UI server is an easier build target than all the browsers in use, but can still handle a variety of client devices.
- At the time, I thought that implementing the UI layer on the server might be faster, since I hadn’t built an app using a client-side JS framework yet.
- The architecture could be more isomorphic, since JS would be written on both the client and server.
The API server(s) were going to stay in Rails/Sinatra, but Rails was obviously not a good choice for the UI layer. Since I had been working with NodeJS and had a positive experience working in that ecosystem, it seemed like a good candidate. As described in “Developing mobile apps with Node.js and MongoDB”:
The new Node project would run alongside Rails as we built it out. Rails was the catch-all to start with; nginx would be configured to redirect traffic to Node as functionality was added. Nginx could also be used to serve the static assets (bootstrap code, images, etc).
Alas, the business closed down shortly after these ideas were formed. I’m not sure how well it would have worked. Frameworks like Angular are pretty fast and easy to write for nowadays. Throwing in a special layer for non-JS clients, whether it’s a service like prerender.io or your own PhantomJS implementation, might be the easier solution.