Introduction

This is part of a series of tutorials on APIs and the React library. See the full list at Tutorials. Throughout this series we build a Create-Read-Update-Delete (CRUD) application with a React front end and a Ruby on Rails back end. Each section is self-contained.

The first tutorial in this series showed you how to build a Rails Application Programming Interface (API). The second tutorial added the React front end. This section will show you how to deploy it to Heroku. We will build the whole app again with some changes for production.


The Back End: Build a Ruby on Rails API-only Application

A Rails API-only app is a slimmed down version of a regular Rails web app, skipping the Asset Pipeline and generation of view and helper files. You can read the details in the Rails Guides - API App.

From the command line create a directory for your app, then generate a Rails app using the --api flag to make it API only.

  • mkdir appname; cd appname
  • If you are using RVM for Ruby and Gems version control you can set your gemset at this point.
  • rails new . --api
  • In production we want to use the PostgreSQL database instead of the Rails default SQLite. For a real app we would want to use the same database for development, test and production. For a practice app like this, it's fine to use SQLite on our local environment. Move the sqlite3 gem into the development and test group. Then create a production group and add the pg gem.

    # Gemfile
    group :development, :test do
      gem 'sqlite3'
      ...
    end
    group :production do
      gem 'pg' 
    end
    
  • bundle install
  • Change the database.yml file to the below. For the Production section, replace "appname" with the name of your app.

    # config/database.yml
    development:
      adapter: sqlite3
      pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
      timeout: 5000
      database: db/development.sqlite3
    
    test:
      adapter: sqlite3
      pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
      timeout: 5000
      database: db/test.sqlite3
    
    production:
      adapter: postgresql
      encoding: unicode
      pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
      database: appname_production
      username: appname
      password: <%= ENV['APPNAME_DATABASE_PASSWORD'] %>
    

    Create an Article resource.

  • rails generate scaffold Article title:string content:text
  • rails db:migrate
  • Add some seed data to db/seeds.rb

    # db/seed.rb
    articles = Article.create([
      {title: "Learn Ruby", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
      {title: "Learn Rails", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
      {title: "Learn React", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}
    ])
    
  • rails db:seed
  • Add a namespace to the api. We'll use "api" but it could be any name. Using a namespace avoids collisions between the rails routes and the react app's routes. Defining the namespace as a scope means it only affects the route and you don't need to add a namespace to the controller.

    # config/routes.rb
    scope '/api' do
      resources :articles
    end
    
  • Run the server. The API and the React front end need to run on different ports. For the API we'll use port 3001.
  • rails server -p 3001
  • View the JSON data at http://localhost:3001/api/articles

  • Create the React app

    We'll use the create-react-app npm package to build the React app within our Rails API project in a directory named "client". I'll skip the detailed explanations since they are already covered in the React CRUD app tutorial. From the Rails API project's root directory run:

  • create-react-app client
  • cd client
  • This will add a directory named "client" and install the react app and all the npm packages inside it.

    Install the react-router-dom and axios packages:

    yarn add react-router-dom axios

    Run the React app. Adding the cwd option followed by the path from the current working directory allows you to run it without having to cd into the client folder.

  • yarn start
  • And this should open your browser to localhost:3000 and greet you with the Welcome to React web page including a spinning logo.

    Connect the React App in the client directory to the Rails API

    In the client/package.json file add a proxy property telling the React app to call port 3001.

    # client/package.json
    {
      "name": "client",
      "version": "0.1.0",
      "private": true,
      "proxy": "http://localhost:3001",
      ...
    }
    

    File Structure

    Use UNIX commands to create the file structure. Make sure you are in the client directory. Note, we are moving the React favicon.ico to the images folder but in a real app you would of course put your own favicon image there.

    mkdir public/images
    mv public/favicon.ico public/images/favicon.ico
    mkdir src/images
    mkdir src/stylesheets
    mkdir src/components
    mv src/logo.svg src/images/logo.svg
    mv src/index.css src/stylesheets/index.css
    mv src/App.css src/stylesheets/App.css
    mv src/App.js src/components/App.jsx
    mv src/App.test.js src/components/App.test.js
    touch src/components/Home.jsx
    touch src/components/ArticleList.jsx
    touch src/components/ArticleInfo.jsx
    touch src/components/ArticleAdd.jsx
    touch src/components/ArticleEdit.jsx
    

    We temporarily broke our app. Fix the broken links and imports to get it working again:

    # public/index.html 
    <link rel="shortcut icon" href="%PUBLIC_URL%/images/favicon.ico">
    
    # src/index.js
    import './stylesheets/index.css';
    import App from './components/App';
    
    # src/components/App.jsx
    import logo from '../images/logo.svg';
    import '../stylesheets/App.css';
    

    Save each file after you make the changes, then go check the browser. The app should be working again.

    Stylesheets

    Now let's make our own React content. Start by removing the css classes from App.css.

    # src/stylesheets/App.css
    /* Remove all the CSS classes */
    

    For convenience we'll use the Bootstrap CDN to style our app:

    # client/src/stylesheets/index.css 
    @import url('https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css');
    

    Create the React components

    # public/index.html

    This is a Single-Page Application (SPA) with CRUD capabilities and this is the single page. It is a standard HTML page with one element in the body. A div with id of "root" which is where all our React components will be displayed via JavaScript.

    # src/index.js

    Then in the src/index.js file we import the React library and render our main component. Your src/index.js file should look like this.

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './stylesheets/index.css';
    import App from './components/App';
    import * as serviceWorker from './serviceWorker';
    
    ReactDOM.render(<App />, document.getElementById('root'));
    serviceWorker.unregister();
    

    # src/components/Home.jsx

    Let's start out with a very simple component called Home. We already created an empty Home.jsx file. Populate it with the below:

    import React from 'react';
    
    const Home = () => {
      return (
        <div className="jumbotron">
          <h1>Home Page</h1>
        </div>
      );
    }
    
    export default Home;
    

    # src/components/App.jsx

    The central component for our app is "App". That is what is returning the Welcome to React page we see in the browser. Replace that with a Navigation bar and a place to render the other components.

    import React, { Component } from 'react';
    import '../stylesheets/App.css';
    import Home from './Home';
    import ArticleList from './ArticleList';
    import ArticleInfo from './ArticleInfo';
    import ArticleAdd from './ArticleAdd';
    import ArticleEdit from './ArticleEdit';
    import {BrowserRouter as Router, Route, NavLink, Switch} from 'react-router-dom'
    
    class App extends Component {
      render() {
        return (
          <div className="App">     
            <Router>
              <div className="container">
                <Navigation />
                <Main />
              </div>
            </Router>
          </div>
        );
      }
    }
    
    const Navigation = () => (
      <nav className="navbar navbar-expand-lg navbar-dark bg-dark">
        <ul className="navbar-nav mr-auto">
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li>
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li>
        </ul>
      </nav>
    );
    
    const Main = () => (
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/articles" component={ArticleList} />
        <Route exact path="/articles/new" component={ArticleAdd} />
        <Route exact path="/articles/:id" component={ArticleInfo} />
        <Route exact path="/articles/:id/edit" component={ArticleEdit} />
      </Switch>
    );
    
    export default App;
    

    Once you add the code and save the file, go to the browser and make sure you didn't get any errors. You should see a navigation bar with two links, and a simple home page jumbotron. If you click on the articles link you'll get a blank page because we haven't populated that yet.


    # src/components/ArticleList.jsx

    Populate the ArticleList component. This returns the equivalent of the Rails Articles index page. We call the API to retrieve a list of all the articles in the database. We set a proxy in the package.json file so we don't need to add the full URL in our API calls, just the path.

    import React, { Component } from 'react';
    import { get } from 'axios';
    import { Link } from 'react-router-dom';
    
    class ArticleList extends Component {
      constructor() {
        super();
        this.state = { articles: [] };
      }
    
      componentDidMount() {
        get('/api/articles.json')
          .then(response => { 
            this.setState({articles: response.data});
          })
          .catch(error => console.log('error', error));
      }
    
      render() {
        return (
          <div>
            {this.state.articles.map((article) => {
              return(
                <div key={article.id}>
                  <h2><Link to={`/articles/${article.id}`}>{article.title}</Link></h2>
                  {article.content}
                  <hr/>
                </div>
              )     
            })}
            <Link to="/articles/new" className="btn btn-outline-primary">Create Article</Link>  
          </div>
        )
      }
    }
    
    export default ArticleList;
    

    Check to make sure it works. Make sure the server is running on port 3001.

    rails server -p 3001

    In the browser on localhost:3000 if you click on the articles link you should see a list of articles. This corresponds with the JSON view at http://localhost:3001/api/articles.json which you can view in a separate browser tab.


    Foreman (Run the API and Client servers with one command)

    Rather than changing into the client directory to start the React app, you can use the change-working-directory option to run it from the Rails product root directory like this: yarn --cwd client start. That still requires you to open two Terminal windows and start two separate servers. Instead, you can add the Foreman gem to run them both with one command. Foreman is a utility you call from the Terminal so you save it directly to your system, not though your Gemfile.

  • gem install foreman
  • Then create a Procfile for your development environment and populate it with the below.

  • touch Procfile.dev
  • # Procfile.dev
    web: cd client && PORT=3000 yarn start
    api: PORT=3001 && bundle exec rails s
    

    By default Foreman looks for a file called Procfile to execute. The force option -f tells Foreman to use the provided file name instead. To start both servers run:

  • foreman start -f Procfile.dev

  • # src/components/ArticleInfo.jsx

    On to the ArticleInfo component which would correspond in Rails with the Articles show page. Populate the file with the below and save it.

    import React, { Component } from 'react';
    import axios from 'axios';
    import { Link } from 'react-router-dom';
    
    class ArticleInfo extends Component {
      constructor() {
        super();
        this.state = { article: {} };
        this.handleDelete = this.handleDelete.bind(this);
      }
    
      componentDidMount() {
        axios.get(`/api/articles/${this.props.match.params.id}.json`)
          .then((response) => { 
            this.setState({
              article: response.data
            })
          })
          .catch(error => console.log('error', error));
      }
    
      handleDelete() {
        axios.delete(`/api/articles/${this.props.match.params.id}.json`)
          .then(() => {
            this.props.history.push("/articles")
          })
          .catch(error => console.log('error', error));
      }
    
      render() {
        return (
          <div>
            <h2>{this.state.article.id}: {this.state.article.title}</h2>
            <p>{this.state.article.content}</p>
            <p>
              <Link to={`/articles/${this.state.article.id}/edit`} className="btn btn-outline-dark">Edit</Link> 
              <button onClick={this.handleDelete} className="btn btn-outline-dark">Delete</button> 
              <Link to="/articles" className="btn btn-outline-dark">Close</Link>
            </p>
            <hr/>
          </div>
        )
      }
    }
    
    export default ArticleInfo;
    

    Now test it out in the browser. From the Articles page, if you click on an article title it should take you to the article page.


    # src/components/ArticleAdd.jsx

    ArticleAdd is the React equivalent of the Rails articles new page.

    import React, { Component } from 'react';
    import { post } from 'axios';
    
    class ArticleAdd extends React.Component {
      constructor() {
        super();
        this.state = { title: '', content: ''};
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleCancel = this.handleCancel.bind(this);
      }
    
      handleSubmit(event) {
        event.preventDefault();
        post('/api/articles.json', this.state)
          .then((response) => {
            this.props.history.push(`/articles/${response.data.id}`);
          })
          .catch(error => console.log('error', error));
      }
    
      handleChange(event) {
        this.setState({ [event.target.name]: event.target.value });
      }
      
      handleCancel() {
        this.props.history.push("/articles");
      }
    
      render() {
        return (
          <div>
            <h1>Create Article Post</h1>
            <form onSubmit={this.handleSubmit}>
              <div className="form-group">
                <label>Title</label>
                <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="form-group">
                <label>Content</label>
                <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="btn-group">
                <button type="submit" className="btn btn-dark">Create</button>
                <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button>
              </div>
            </form>
          </div>
        );
      }
    }
    
    export default ArticleAdd;
    

    # src/components/ArticleEdit.jsx

    ArticleEdit is the React version of Rails articles edit page.

    import React from 'react';
    import { get, patch } from 'axios';
    
    class ArticleEdit extends React.Component {
      constructor() {
        super();
        this.state = { title: '', content: ''};
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleCancel = this.handleCancel.bind(this);
      }
    
      componentDidMount() {
        get(`/api/articles/${this.props.match.params.id}.json`)
          .then((response) => {
            this.setState(response.data);
          })
          .catch(error => console.log('error', error));      
      }
    
      handleSubmit(event) {
        event.preventDefault();
        patch(`/api/articles/${this.state.id}.json`, this.state)
          .then(() => {
            this.props.history.push(`/articles/${this.state.id}`);
          })
          .catch(error => console.log('error', error));
      }
    
      handleChange(event) {
        this.setState({ [event.target.name]: event.target.value });
      }
    
      handleCancel() {
        this.props.history.push(`/articles/${this.state.id}`);
      }
    
      render() {
        return (
          <div>
            <h1>Edit {this.state.title}</h1>
            <form onSubmit={this.handleSubmit}>
              <div className="form-group">
                <label>Title</label>
                <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="form-group">
                <label>Content</label>
                <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="btn-group">
                <button type="submit" className="btn btn-dark">Update</button>
                <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button>
              </div>
            </form>
          </div>
        );
      }
    }
    
    export default ArticleEdit;
    

    Save it and test it out. We are now done with our React components and we have a functionining CRUD application, at least in our development environment.


    Deploy to Heroku

    Add package.json to the Rails project's root directory

    Add a package.json file to the root of your project. yarn init or npm init will create a basic one and ask you a series of questions for populating it. Adding the --yes flag will use the default answers which is fine for our purposes.

  • yarn init -y
  • So now we have a package.json file in our client directory for React and in the root directory of our Rails app. Open the latter that we just created and add engines and scripts properties.

    # package.json
    {
      "name": "appname",
      "version": "1.0.0",
      "main": "index.js",
      "author": "your_name",
      "license": "MIT",
      "engines": {                          #1
        "node": "10.16.0",
        "yarn": "1.16.0"
      },
      "scripts": {                          #2
        "build": "yarn --cwd client install && yarn --cwd client build",
        "deploy": "cp -a client/build/. public/",
        "heroku-postbuild": "yarn build && yarn deploy"
      }
    }
    
    1. Engines:
      Get the latest node and yarn versions. Running npm show package-name version will return the latest package versions. But for node you might want to use the more conservative LTS version which you can get from nodejs.org.
    2. Run Scripts:
      We need to tell Heroku to install the React packages in the client folder and call the build command to build the React app.
      The deploy command will move the React app to the public folder of your Rails app.
      The heroku-postbuild command will run the build and deploy scripts above after

    Procfile

    Heroku uses the Foreman gem which by default will look for a file named "Procfile". We can put some instructions to use the puma server (or a different server if you prefer) and what port to run on.

  • touch Procfile
  • # Procfile
    web: bundle exec puma -t 5:5 -p ${PORT:-3001} -e ${RACK_ENV:-development}
    

    Deploy your app to Heroku

    This tutorial assumes you are already familiar with Heroku. If not, you need to first create an account on Heroku: heroku.com
    Then download and install the Heroku Command Line Interface on your computer devcenter.heroku.com/articles/heroku-cli.

    From the CLI, create a new Heroku App:

  • heroku create appname
  • If your appname is already taken you'll have to pick another one. Or leave off the appname and let Heroku generate a super awesome one for you. If you have a real domain name you can configure Heroku to use that devcenter.heroku.com/articles/custom-domains

    Check that your app is connected to Heroku. The below command will list the project's remote git repositories (if any) and their urls. Rails automatically initiates a git repository when you create an app (so you don't need to run git init)

  • git remote -v
  • Tell Heroku to execute the Node.js buildpack before the Ruby buildpack. So in the CLI run:

  • heroku buildpacks:add heroku/nodejs --index 1
  • heroku buildpacks:add heroku/ruby --index 2
  • Commit all your changes to git then push the app to Heroku:

  • git add .
  • git commit -m "initial commit"
  • git push heroku master
  • Run the database migrations then seed the database. Heroku automatically adds the Heroku Postgres addon for you.

  • heroku run rake db:migrate
  • heroku run rake db:seed
  • Assuming you got no errors in the above processes, you can run:

  • heroku open
  • The app will open to https://appname.herokuapp.com.
    To see the API JSON data go to https://appname.herokuapp.com/api/articles