ReactJS + Ruby on Rails API + Heroku App

Bruno Boehm
Mar 24, 2018 · 11 min read

A few notes from building a React Front-end with Rails API on the same stack, with basic CRUD functionalities, and uploading it to Heroku.

Based on my own experiments, and the following sources :

1. Create the rails App API structure

mkdir list-app
cd list-app
rails new . --api --database=postgresql -T --no-rdoc --no-ri

-T flag to tell the Rails project generator not include TestUnit, the default testing framework. This will leave room for us to use ‘rspec’

Start Postgres server by opening the postgress app on computer

rails s -p 3001

Fire up Rails server and see the welcome page


2. Create the React client

Inside the application folder, create the client folder using create-react-app client.

Now inside of the client folder, you can use yarn to do a bunch of stuff, for example :

  • yarn start (or npm start) : Starts the development server. You can access the React page on http://localhost:3000/
  • yarn build (or npm build) : Bundles the app into static files for production.
  • yarn test (or npm test) : Starts the test runner.

We now automatically proxy the API calls via the right port, without needing to swap anything between development and production. Once we hook everything up our scripts will be running the API on port 3001.

We now automatically proxy the API calls via the right port, without needing to swap anything between development and production. Restart the NPM server now!


3. Create the main active record relationships

a. List

Let’s scaffold the List resource with rails g scaffold List title:string excerpt:text description:text upvotes:integer The scaffold in API mode gives the following elements: migration, model, route, controller (nearly like a rails g resource cmd)

Running via Spring preloader in process 54716
invoke active_record
create db/migrate/20180113164043_create_lists.rb
create app/models/list.rb
invoke resource_route
route resources :lists
invoke scaffold_controller
create app/controllers/lists_controller.rb

Set up the model like this

class List < ApplicationRecord
has_many :list_items
has_many :items, through: :list_items
end

b. Item

Similarly we create the Item resource rails g scaffold Item type:integer name:string excerpt:text description:text url:string upvotes:integer. The type is an integer since it will be used as an enum.

class Item < ApplicationRecord
has_many :list_items
has_many :lists, through: :list_items
end

c. ListItem

Lastly we can create the join relationship of has_many :through with the command rails g model ListItem list:references item:references description:text position:integer.

This will only create the migration and model

Running via Spring preloader in process 54969
invoke active_record
create db/migrate/20180113165518_create_list_items.rb
create app/models/list_item.rb

We use references to create the index as well as the belongs_to in the model

class ListItem < ApplicationRecord
belongs_to :list
belongs_to :item
end

4. Create DB and migrate, seed some data

We can now create the postgresql DB and migrate using rake db:createand rake db:migrate. Note in the schema we now have for index

t.index ["item_id"], name: "index_list_items_on_item_id"
t.index ["list_id"], name: "index_list_items_on_list_id"

Let’s now add some data in db/seeds.rb.

List.create(title:"West Sweden Road Trip", excerpt:"A cool road trip with stops in harbors of the coast")
List.create(title:"Must have equipment for the outdoor photographer", excerpt:"My selection of gear for modern outdoor photography")

5. Routes and Controllers

Let’s put all URLs behind an “api” namespace, and version our API “v1” (good practice).

namespace :api do
namespace :v1 do
resources :items
resources :lists
end
end

With this namespacing, we need to reflect that as a structure for accessing our controllers.

Let’s create a app/controllers/api/v1 folder and copy out lists_controller.rb and items_controller.rb files there (generated by the scaffold. Let’s wrap each of the 2 controllers into a module Api::V1

module Api::V1
...
end

We now have the followinglists_controller.rb (based on the scaffold)

We can now start our API endpoint by asking for the seeded list of lists (after we’ve restarted the puma server): http://localhost:3001/api/v1/lists. We got served our JSON. Great.

Note we’ve set up in previous step a proxy for asking all calls beginning with “/api” to reach localhost:3001 (where the Rails API is running), and not :3000 (where our NPM React client is at).

"proxy": {
"/api": {
"target": "http://localhost:3001"
}
},

6. Creating the first React Component: ListsContainer

Let’s change the App.js main component a little to import and call the <ListsContainer /> component.

And create a “client/src/components” folder for us to store our components. We create our ListsContainer.js component in there


7. Fetching API Data with Axios

Let’s install axios to make our API calls (instead of the original fetch that has a few limitations): npm install axios.

We can now import it into our ListsContainer by adding the opening line import axios from 'axios';. You might have to redo a npm install and quit + restart the npm server with npm start.

Let’s now initialize the state and use our new tool in the componentDidMount() lifecycle event. We should have something like :

import React, { Component } from 'react';
import axios from 'axios';
class ListsContainer extends Component {
constructor(props){
super(props)
this.state = {
lists: []
}
}
componentDidMount() {
axios.get('http://localhost:3001/api/v1/lists.json')
.then(response => {
console.log(response)
this.setState({
lists: response.data
})
})
.catch(error => console.log(error))
}
render() {
return (
<div className="Lists-container">
Lists
</div>
)
}
}
export default ListsContainer;

If our proxy if well configured we have no error. Otherwise it will throw into the javascript console the following CORS error : “Failed to load http://localhost:3001/api/v1/lists.json: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:3000' is therefore not allowed access.”. Make sure the proxy is setup and restart the NPM server for changes to take effect.

Now that we are able to receive API data, let’s change our component’s render function to iterate throught the lists from the state and display each of them.

We now have this component structure:

Here we are, it’s connected!!!

8. Refactoring using a stateless/dumb component: List

Let’s simplify by grouping our List code into a simple component that does not handle any state put receives data by props.

We create a List.js component like this

import React from 'react';const List = ({list}) =>
<div className="single-list" key={list.id}>
<h4>{list.title}</h4>
<p>{list.excerpt}</p>
</div>
export default List;

And import it from our main ListContainer component import List from './List';, as well has calling it in the render function.

render() {
return (
<div className="lists-container">
{this.state.lists.map( list => {
return (<List list={list} key={list.id} />)
})}
</div>
)
}

9. Into the CRUD (Create Read Update Delete)

a. [C]RUD : CREATE action

Let’s correct our scaffoled controller to remove the “location”. We should have

# POST /lists
def create
@list = List.new(list_params)

if @list.save
render json: @list, status: :created
else
render json: @list.errors, status: :unprocessable_entity
end
end

And let’s add a form to our ListsContainer component (inside of a single div — otherwise error)

<NewListForm onNewList={this.addNewList} />

Let’s create a new file for our NewListForm component using refs.

Now we can add a addNewList function/method to our component, and import NewListForm from './NewListForm';. Note that {title, excerpt} is equivalent with ES6 to writing {title: title, excerpt: excerpt}.

And let’s make sure we bind “this” (from the component) to the method, so that this.state and this.setState can be accessed by the method.

constructor(props){
super(props)
this.state = {
lists: []
}
this.addNewList = this.addNewList.bind(this)
}

b. CRU[D] DELETE action

Let’s start by wiring up our Lists controller for the DELETE action

# DELETE /lists/1
def destroy
@list.destroy
if @list.destroy
head :no_content, status: :ok
else
render json: @list.errors, status: :unprocessable_entity
end
end

Now let’s update the List presentational component to include an onClick handler with the function (from the destructured props with default value) to remove the List

At last in the parent ListsContainer component, let’s write this onRemoveList callback. Let’s make sure to bind this this.removeList = this.removeList.bind(this)

We can now pass down the removeList function by the props to the List component.

<List list={list} key={list.id} onRemoveList={this.removeList} />

We now have a proper DELETE action!

c. CR[U]D : UPDATE action

Let’s add the ability to edit a List in place. Let’s update our controller first

# PATCH/PUT /lists/1    
def update
if @list.update(list_params)
render json: @list
else
render json: @list.errors, status: :unprocessable_entity
end
end

We’ll lift state to the highest component (ListsContainer).

We first need a EditListForm.js component that maintains its own state (a form). It initializes state pased on props when values get passed down. On input change, it updates the state and hence the view. On form submit it calls the editList() function that it gets from the props.

Let’s now take care of updating state higher in the ListsContainer component.

Also note how we use an updater function for setting up the new state with setState when the promise is resolved. It’s a best practice (would be worth updating the rest of the code base too anytime we use setState).

Let’s now work out the part where we enable to edit the list in place. In our ListsContainer we add a new property to the state: editingListId, that will take the id of the list we are currently editing (or null).

constructor(props) {
super(props)
this.state = {
lists: [],
editingListId: null
}
this.addNewList = this.addNewList.bind(this)
this.removeList = this.removeList.bind(this)
this.editingList = this.editingList.bind(this)
this.editList = this.editList.bind(this)
}

Now in our render function we leverage if editingListId and in case it corresponds to the id of one of the lists, we replace the List component by the EditListForm component (that passes down the editList function we can now used from the props in EditListForm's onSubmit action).

And in our List component we need to pass down the editList function:

That’s all there is really! Here’s the final file for our ListContainer component:


10. Interlude: Foreman

Instead of relying on cd client yarn start and in another terminal window rails s, let's install the Foreman gem

gem install foreman

And now let’s create a Profile.dev (we’ll only use this for dev since we don’t need a node server in production — in dev we use if for hot reloading etc.).

web: cd client && PORT=3000 npm start
api: PORT=3001 && bundle exec rails s

Now we can call it by typing foreman start -f Procfile.dev (we use the -f flag for specifying a specific file to run, instead of the default Procfile).

We could also create a rake file like start.rake in our lib/tasks directory to make this even smoother with a single rake start cmd in the terminal.

Note the start:production one will need a package.json in the root of the rails app. Will be handy for checking our production bundled version before deploying, but this task will be ready to be used in the next chapter.

We should now get an error typing bin/rake start like "foreman is not part of the bundle. Add it to Gemfile. (Gem::LoadError)". We need to add foreman (with latest version for supporting cd) to our gemfile for this, in the :development group.

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'foreman', '~> 0.82.0'
end

We can now run bundle install to update our gems.


11. Deploying to Heroku

Let’s prepare our app for production, with Rails serving the Webpack bundle. Using NPM’s postinstall command (that runs after npm is installed) from a package.json file in the app's root, we will have Heroku npm run build the react app, and then move it to /public served by Rails. We end up with a single Rails server managing both front-end and back-end.

a. Postinstall magic

Heroku recognizes a node.js application when it sees a package.json file in the root of an application. Let's create this file (can use npm init).

{
"name": "react-and-rails",
"engines": {
"node": "6.3.1"
},
"scripts": {
"build": "cd client && npm install && npm run build && cd ..",
"deploy": "cp -a client/build/. public/",
"postinstall": "npm run build && npm run deploy && echo 'Client built!'"
}
}

Heroku will start with npm install that will install all necessary npm dependencies, then, will run postinstall (that runs build (builds our create-react-acpp) and deploy (copies content from the client/build to /public) as we defined them.

b. Heroku app creation

Let’s create a new Heroku app with heroku apps:create. Confirmation

Creating app... done, ⬢ polar-roof-12345
https://polar-roof-12345.herokuapp.com/ | https://git.heroku.com/polar-roof-12345.git

Let’s now tell Heroku to start by building the node app using package.json, and then build the rails app.

heroku buildpacks:add heroku/nodejs --index 1
heroku buildpacks:add heroku/ruby --index 2

This uses Heroku’s buildpacks which are processes to build your code for Heroku’s dynos. We should get a confirmation in the terminal:

Buildpack added. Next release on polar-roof-12345 will use:
1. heroku/nodejs
2. heroku/ruby
Run git push heroku master to create a new release using these buildpacks.

We need a Procfile in our root for production to tell (this was a step we needed for our rake start:production remember?) how to start the rails app.

web: bundle exec rails s

Note from Heroku: “The name web is important here. It declares that this process type will be attached to the HTTP routing stack of Heroku, and receive web traffic when deployed."

We can now test our production build locally with our Rake task rake start:production, that will end up running foreman (with our new Procfile) and kickstart the rails server... that gets it's scripts from the /public folder (that previously only contained a robot.txt file).

We can update the root’s .gitignore to make sure the /public folder doesn't get included in version control.

# Production
/public

Last thing is to change our react-scripts from devDependencies to dependencies. Why? Because Heroku sets an ENV var, NPM_CONFIG_PRODUCTION, to true, which means your build will disregard any devDependencies and it will fail, nicht gut.

Now we are ready to push this out to Heroku.

git add .
git commit -m "ready for first push to heroku"
git push heroku master

Let’s watch the build happen

Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 415 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
remote:
remote: NPM_CONFIG_LOGLEVEL=error
remote: NODE_VERBOSE=false
remote: NODE_ENV=production
remote: NODE_MODULES_CACHE=true
remote:
remote: -----> Installing binaries
remote: engines.node (package.json): 6.3.1
remote: engines.npm (package.json): unspecified (use default)
remote:
remote: Resolving node version 6.3.1...
remote: Downloading and installing node 6.3.1...
remote: Using default npm version: 3.10.3
remote:
remote: -----> Restoring cache
remote: Loading 2 from cacheDirectories (default):
remote: - node_modules
remote: - bower_components (not cached - skipping)
remote:
remote: -----> Building dependencies
remote: Installing node modules (package.json)
remote:
remote: > react-and-rails@1.0.0 postinstall /tmp/build_0548498aa5df59a8adf33e204138f141
remote: > npm run build && npm run deploy && echo 'Client built!'
remote:
remote:
remote: > react-and-rails@1.0.0 build /tmp/build_0548498aa5df59a8adf33e204138f141
remote: > cd client && npm install && npm run build && cd ..
remote:
...
remote: Bundle complete! 9 Gemfile dependencies, 40 gems now installed.
remote: Gems in the groups development and test were not installed.
remote: Bundled gems are installed into ./vendor/bundle.
remote: Bundle completed (24.97s)
remote: Cleaning up the bundler cache.
remote: -----> Detecting rake tasks
remote:
remote: ###### WARNING:
remote: You have not declared a Ruby version in your Gemfile.
remote: To set your Ruby version add this line to your Gemfile:
remote: ruby '2.3.4'
remote: # See https://devcenter.heroku.com/articles/ruby-versions for more information.
remote:
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote: Default types for buildpack -> console, rake, worker
remote:
remote: -----> Compressing...
remote: Done: 68.3M
remote: -----> Launching...
remote: Released v5
remote: https://polar-roof-12345.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.

Yay.

Let’s not forget to create and seed the database…

heroku run rake db:migrate
heroku run rake db:seed

12. Bonus Level : React Router on Heroku

Haven’t tested this one out yet, but based on the question by Logan Gants here’s the gist.

We need add a fallback_index_html to our ApplicationController:

class ApplicationController < ActionController::Base
def fallback_index_html
render :file => 'public/index.html'
end
end

And at the bottom of routes.rb:

get '*path', to: "application#fallback_index_html", constraints: ->(request) do
!request.xhr? && request.format.html?
end

That way Rails will pass anything it doesn’t match to your index.html so that react-router can take over.

Bruno Boehm

Written by

Co-founded Lyketil.com Digital Lab | Ex.Digital @SolarImpulse | EMLYON MSc | Flatiron School Dev

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade