Universal React with Rails: Part I

react on railsApril 19, 2015Dotby Alex Fedoseev

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:

image1

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

image3

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

image2

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

Part II: Building JSON API

Part III: Building Universal app

Part IV: Making Universal Flux app

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right