Universal React with Rails: Part I
Planning the application
I’d like to share intermediate results of my work with Universal (aka “Isomorphic”) JavaScript apps, based on React library from Facebook and Ruby on Rails as backend.
Actually it’s not so much about Rails, but about JSON API. So if you don’t use/like Rails, just take it as an abstract API and keep reading.
If you haven’t heard about isomorphic javascript concept, here is the link that explains what it’s all about.
Within current post we’ll plan application architecture. In the next one we’ll setup Rails JSON API. After that we’ll kick-start universal javascript app. Before we’ll share it with the world, we’ll secure it. And in the end everything will be deployed to production.
The plan
I’ve been trying to implement JS server rendering within Rails app using react-rails gem, but it’s not the way to go. Tools plays best in environments, for which they were designed. So I cut whole front-end stuff out of Rails and moved it to Node.js.
Here is the first draft:
Rails, which became simply JSON API, is responsible for data and React handles the interface part, using the same javascript codebase on the server and on the client. Worth to note that we can use Rails as API for mobiles apps as well.
Let’s go deeper. When user hits http://my-app.com, request goes to the Node.js server with Express on top. We need the data to render initial html and send the response. So we fetch it from Rails API via http-request. When the data is arrived, React renders the view and Express sends html to the user with client JS app, which takes control over the flow. When the user hits some link on the site, app makes the ajax call to fetch the new data and updates the views on the client.
Front-end app lives on the main domain — http://my-app.com. But for the API we have an options:
-
my-app.com:3000
We can have it on the same domain, but on the different port. Then we are forced to keep 2 apps on the same server.
-
api.my-app.com
I prefer another option — put it on the subdomain (or another domain), so we can scale app in the future without changing the codebase.
Next we need to decide how we’ll be getting the data from API via ajax requests.
Option 1. Ajax CORS requests
Because of the same-origin policy we can’t make ajax requests from one location to another (even if the target resource on the same domain but different port). To enable such kind of requests we need to apply Cross-Origin Resource Sharing (CORS) mechanism.
If you’ll chose to go this way, you’ll need to set special http-headers on the Rails side. This way browser is verifying that caller have the rights to send the ajax requests to this server.
In application_controller.rb do something like this:
class ApplicationController < ActionController::API
before_action :set_origin
before_action :set_headers
private
def set_origin
@origin = request.headers['HTTP_ORIGIN']
end
def set_headers
if @origin
allowed = ['lvh.me', 'localhost', 'my-app.com']
allowed.each do |host|
if @origin.match /^https?:\/\/#{Regexp.escape(host)}/i
headers['Access-Control-Allow-Origin'] = @origin
break
end
end
# or '*' for public access
# headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
headers['Access-Control-Request-Method'] = '*'
headers['Access-Control-Allow-Headers'] = 'Content-Type'
end
end
end
Keep in mind that by enabling CORS (especially public access) you dig potential CSRF security hole. Read this carefully to mitigate CSRF attacks.
Option 2. Proxy ajax calls through front-end server
Instead of enabling CORS, we can proxy requests through nginx front-end server. Every call goes to the same domain: my-app.com. Nginx splits it in two directions:
-
my-app.com/*
Almost all of requests are passed to Node.js app.
-
my-app.com/api/*
Except calls to '/api', which are proxied to Rails API.
This approach is more secure and the whole system looks solid from outside. So we will go this way, but it requires some additional setup on local machine.
Local setup
Install nginx:
$ brew install nginx
Edit nginx.conf:
$ sudo nano /usr/local/etc/nginx/nginx.conf
Extend http block in config with upstreams (notice comments on the right):
http {
upstream app_proxy {
server lvh.me:3500;
}
upstream api_proxy {
server api.lvh.me:3000;
}
server {
listen 80;
server_name lvh.me;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://app_proxy;
proxy_redirect off;
}
location /api {
proxy_set_header Host api.lvh.me;
proxy_set_header X-forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://api_proxy/;
proxy_redirect off;
}
}
server {
listen 80;
server_name api.lvh.me;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://api_proxy;
proxy_redirect off;
}
}
}
Finally, run nginx:
$ sudo nginx
Now you can visit http://lvh.me in your browser. There will be nginx error message. It’s ok, because Node.js app is not yet running.
Stop nginx for now:
$ sudo nginx -s stop
Hosts
Also you may want to add lvh.me to hosts file to avoid unnecessary roundtrips:
$ sudo nano /private/etc/hosts
And add:
fe80::1%lo0 lvh.me
fe80::1%lo0 api.lvh.me
127.0.0.1 lvh.me
127.0.0.1 api.lvh.me
Conclusion
At this point we have a plan how to build the app. Next time we’ll setup Rails API, which will be handling application’s data.
Stay tuned!
Part I: Planning the application