Learn how to implement a Rails and React monorepo that achieves the perfect balance between speed and sustainability.
Software development. It’s always about trade-offs, isn’t it? In the beginning, the top priority is getting your idea in front of users. So, you should build it as fast as you can, cutting as many corners as you can, right? But if you do that, you sacrifice longer-term sustainability. Ideally, you want to build a product in a way that allows you to quickly and easily add features and support more traffic while ensuring the system stays stable, without the need for large refactors or rewrites.So, how do you achieve a good balance between speed and sustainability? At Smashing Boxes, these are some of the factors we consider:
- Productivity – Every member of the team, no matter their core discipline, should be able to work on their own features, without needing help from others. That said, team members should be able to collaborate easily and put their respective pieces of each feature together.
- Decoupling – The application needs to be written in such a way that it’s easy to swap out parts of it. For example, if product discovery reveals that the backend logic is mostly right, but the frontend needs to move in a different direction, we should be able to make significant changes quickly without impacting the app's business logic.
- Reusability – The application also needs to be written in such a way that new requirements can leverage existing functionality, without too much effort. An example here would be if the application starts out as a web application, but eventually, you need to have a mobile app. The backend should be able to handle that requirement without needing to rebuild anything.
- Testability – As features are added and changed, you want to know that none of the existing functionality is breaking. Having a good test suite is key to this, especially end-to-end functional tests. These tests should be created alongside the features they’re testing, and run every time a new change is made.
- Continuous Deployment – Deployment should be as easy and painless as possible. There should be no additional steps outside of the normal development workflow, and there should be minimal configuration to manage. Additionally, each feature should be able to be developed as a single unit, and we should be able to deploy the entire feature at once.
Let's discuss a solution that covers all these bases: A single repository, with Ruby on Rails for the backend API, and React.js for the front-end user interface.
Rails and React Monorepo Implementation
The root of the repository is a standard Rails application, created with rails new. Alongside all the rails code, at the root level we create a new folder named “frontend”, containing the React application (created with create-react-app, for example).The next step is to get these two apps talking to each other. To do that, we’ll configure the React application to compile directly into the public folder of the rails application, by changing the build command in package.json to: react-scripts build && mv build ../publicNext, we’ll configure Rails to serve up the frontend. To do that, we need two things. First is a route fallback, at the bottom of our routes file:Rails.application.routes.draw do
# ...
get "*path", to: "application#frontend", constraints: ->(request) { frontend_request?(request) }
def frontend_request?(request)
!request.xhr? && request.format.html?
end
end
All of the routes above the fallback will behave like normal. That’s where anything like API endpoints or an admin panel would go. If the route does not match any of those, Rails will fall back to serving up the React application. That means that the React application will need to handle a 404 page, for example.Next, we need to set up the corresponding index action in our ApplicationController, and configure it to serve up the index.html file created by the React app:class ApplicationController < ActionController::Base
# ...
def frontend
render file: "public/index.html", layout: false
end
end
That’s it! There are a few more steps to get our end-to-end functional tests working in this type of set up, but I won’t get into that here because it depends on what test system we are using. We really like capybara with site_prism, but there are many great solutions.
How Rails and React Monorepo Compares to Other Options
Of course, there are many other ways to structure web applications. Each of them solves various parts of this problem, but a rails and react monorepo is the best way we’ve found to solve all of them and hit a perfect balance between speed and sustainability. That said, I want to highlight a few alternatives and where they fall short.
“Pure” Ruby on Rails
Ruby on Rails is a great tool for building applications quickly. In a matter of hours, you can go from nothing to a basic application, complete with database, URL routes, frontend views, async worker jobs, a great testing framework, and much more. Add in Heroku as a hosting platform, and it can be deployed in minutes. That said, Rails falls short in one key area: the view layer. In a typical “pure” Rails app, the way data is passed from the controller to the view is via an instance variable. Those instance variables are full-fledged Ruby objects (usually ActiveRecord models), which is both a pro and a con. On one hand, it means we can leverage Ruby and Rails’ great methods, which make it easy to manipulate and format data on the fly. On the other hand, the views are inherently coupled to that object.Additionally, new frontends and mobile apps cannot consume the HTML output of Rails views. Instead, we would need to build out a new API for them to consume, so “pure” Rails fails the reusability requirement as well.Productivity – ✅Decoupling – ❌Reusability – ❌Testability – ✅Continuous Deployment – ✅
React Components Rendered via Rails Views
With the rise of React, new tools have been created that make it very easy to integrate React into Rails views. In fact, as of Rails 5.1, webpacker is built-in and included in new apps. Two other examples that I really like are react-rails and react_on_rails. I tend to think of all of these as alternatives to Rails’ built-in templating engine.Regardless of the specific way we do this, there’s one major benefit with this option, over Rails views: decoupling. The way we pass data into these components is via a JSON object, which gets turned into props for the React component. That conversion to JSON means the frontend is inherently decoupled from the controller, with that JSON object being the interface between the two.However, just because the application is decoupled does not mean it is reusable. The JSON being produced is not accessible directly, and so it can’t be reused by new frontends. That means separate API endpoints would have to be created to support those new frontends.Productivity – ✅Decoupling – ✅Reusability – ❌Testability – ✅Continuous Deployment – ✅
Rails API + React SPA, as completely separate apps
Another approach is to create two separate repos: one for the Rails API, and one for the React frontend. This type of architecture definitely gets the benefits of decoupling and reusability, because all communications are happening over an API. However, we lose some things.First, we lose some productivity. Team members can work independently, but when it comes time to combine items (such as integrating the frontend with the API), we have two options: A) team members have to figure out how to run each other’s repo, or B) both pieces of code have to be deployed to a shared environment. Neither of these options is ideal.Second, we lose testability. We can create unit tests in each of the separate repos, but when it comes to testing how those pieces work together, using tools such as using Selenium or Cypress, it becomes cumbersome to get the two pieces talking to each other. It’s also difficult to get those types of tests running in the continuous testing tools on pull requests for each of the repos.Finally, we lose easy continuous deployment. With this type of setup, when would we deploy? If we deploy when a frontend feature is completed, the frontend wouldn’t have the API endpoints it depends on. If we deploy when the backend feature is completed, we would have API endpoints with no user-facing functionality. Additionally, this type of setup requires additional configurations for how to compile and build the various parts and fit them together via something like Nginx.Productivity – ❌Decoupling – ✅Reusability – ✅Testability – ❌Continuous Deployment – ❌
Other Backend Frameworks
There are many other backend frameworks, from NodeJS/Express, to Go, to Java, and more. For the most part, an approach similar to the one outlined above could be implemented in those other frameworks. The part where many of these other frameworks fall short is productivity. I have yet to see a tool or framework that spins up a similar amount of functionality to what Rails provides, as quick as it does. If you know of one, I’d love to hear about it.Productivity – ❌Decoupling* – ✅Reusability* – ✅Testability* – ✅Continuous Deployment* – ✅*Each framework has strengths and weaknesses, so depending on what you choose, you may or may not get these benefits.
Why We Dig the Rails and React Monorepo
This architecture gets many of the benefits of having a single-repo Rails-only application:
- It’s fast to develop, allowing you to ship features quickly.
- It’s easy to deploy and pushes off the need for complicated configuration management until you decide you need it.
- End-to-end functional testing is simple and can be included alongside feature development, ensuring that your features are always working.
- CORS is not an issue, because everything is hosted under the same domain.
It also has these benefits of splitting your app into an API and a frontend:
- The apps are inherently decoupled, allowing you to change easily and swap parts out.
- The API is frontend agnostic, which enables easier development of additional platforms such as mobile.
- It’s easy for specialist developers to hop in and quickly get their bearings.
On top of all that, there are some unique benefits that you only get with this approach:
- It allows for cross-disciplinary work, meaning “full-stack” devs are enabled to work quickly across the various parts of the application.
- It encourages more collaboration, without needing to deploy to a shared “dev” environment.
- It enables the professional development of specialists who are interested in picking up new skills. Or they can simply “stay in their lane” if they want to, and be fully productive.
- If you ever do need to split the two codebases, it’s as simple as moving the frontend directory out and removing a few lines of code.
Productivity – ✅Decoupling – ✅Reusability – ✅Testability – ✅Continuous Deployment – ✅No solution is a one size fits all, but we have found the Rails and React monorepo approach to be excellent for adding value in the ways that are important to us as developers. Do you know of other solutions that meet these needs? We would love to hear about them!If you liked this article, be sure to check out our recent posts:Dev Download: Google I/O 2019 #io2019Android Q is coming. Is your app ready?