React + Rails + MongoDB: how to decouple Frontend and Backend using React and Ruby on Rails

bravemaster619

bravemaster619

Posted on March 22, 2020

React + Rails + MongoDB: how to decouple Frontend and Backend using React and Ruby on Rails

Summary: In this article, I am going to explain how to build a React web page using Ruby on Rails for backend and MongoDB for database.

TLDR: Github repository link: https://github.com/bravemaster619/rails-react

Decoupled structure has many great advantages. Frontend developers don’t need to know what framework and database they are using in backend, as long as they have a decent API documentation.

Backend developers can solely focus on performance and can build a more stable, reliable and bug-free framework. They will get errors only from backend, not from templates! What if the project got bigger and you need to optimize backend for better performace? The backend team can work on API v2 while the website is still live. Once the new API is completed, just change api url from v1 to v2 will do the work! The same thing can be said for frontend optimization and layout changes.

Full stack developers can benefit from decoupled structure, too. Clients often give you designs and flow diagrams and order you to build them from scratch. You can start building frontend pages with mockup data. Once you get to know the main flow and details of the project, it will be much more easier to design database and framework.

In this article, I will show you how to build a decoupled web app using React and Ruby on Rails. I chose MongoDB for database because NoSQL databases are so good for projects with flexible data structures.

Prerequisites:

1. Build a Ruby on Rails Framework

Create a directory named rails-react

$ mkdir rails-react  
$ cd rails-react

Create a new Rails app named backend

$ rails new backend --api --skip-active-record

apiswitch will optimize and remove middlewares and resources from our Rails app. (To find out more, refer to this link: Using Rails for API-only Applications)

skip-active-recordswitch removes ActiveRecord dependency because we won’t need that. We will be using mongoid (Click here for official Mongoid Manual).

Install gem dependencies

Add the following lines before group :development to Gemfile:

# mongoid for MongoDB
gem 'mongoid', '~> 7.0.5'

# cors policy
gem 'rack-cors'

mongoid is the official ODM for MongoDB in Ruby. rack-cors makes cross-origin ajax request possible in our project. Since backend and frontend may run in different port, we need to set cors policy in our Rails app.

Next, we are going to install dependencies:

$ bundle install

Configuration for rack-cors

Add the following lines to config/application.rb:

config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [:get, :post, :options]
  end
end

It allows Rails app to allow requests from any origins with any methods.

Configuration for mongoid

Run the following command in shell:

$ cd bin  
$ rails g mongoid:config

It will create a file mongoid.yml in config directory. You can set a database name, hosts and other options for MongoDB. Default database name is backend_development and the database host is localhost:27017.

Create model and controller

Let’s create a user model. It will have only two fields: name and email.

To make things easier, we will use scaffold, rather than creating a model and a controller individually.

$ rails generate scaffold User name:string email:string

Scaffold will generate migration, model, controller, test suite and routes for a given structure.

After everything is set, you can use the following shell command to run a test server:

$ rails server

Note: Don’t forget to run mongod before initializing a test server.

2. Build a React App

Now you can start building a React app for frontend.

In our project root directory rails-react, run the following command:

$ npx create-react-app frontend

Install node modules

After installing is done, let’s add react-bootstrap for responsibility and smart looks:

$ cd frontend
$ npm i react-bootstrap bootstrap

Add react-toastify for alerts in our app:

npm i react-toastify

Since the frontend has to send AJAX requests to Rails API, we need Axioss:

$ npm i axios

Create a file named config.js in src directory and add the following code:

export const API_HOST = 'http://localhost:3000'

App.js

Modify App.js like the following:

import React from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'react-toastify/dist/ReactToastify.css';
import Root from "./components/Root"
import {ToastContainer} from "react-toastify";
const App = () => (
    <>
        <Root />
        <ToastContainer/>
    </>
)
export default App

Root.jsx

In directory src, create a new directory named components and make a new file Root.jsx. Cut and paste the following code:

import React from "react"
import Axios from "axios"
import { alertService } from '../services/alert'
import SubscribeForm from "./SubscribeForm"
import UserTable from "./UserTable"
import { API_HOST } from "../config"

class Root extends React.Component {

    constructor(props) {
        super(props)
        this.state = {
            name: '',
            email: '',
            sendingRequest: false,
            subscription: false,
        }
        this.changeName = this.changeName.bind(this)
        this.changeEmail = this.changeEmail.bind(this)
        this.subscribe = this.subscribe.bind(this)
    }

    changeName(e) {
        let name = e.target.value
        this.setState({name})
    }

    changeEmail(e) {
        let email = e.target.value
        this.setState({email})
    }

    subscribe() {
        this.setState({
            sendingRequest: true
        })
        if (!this.state.name) {
            return alertService.showError('Please input name!')
        }
        if (!this.state.email) {
            return alertService.showError('Please input email!')
        }
        Axios.post(`${API_HOST}/users`, {
            name: this.state.name,
            email: this.state.email,
        }).then(res => {
            if (res.data && res.data._id) {
                this.setState({
                    subscription: true
                })
            } else {
                alertService.showError('Subscription failed!')
            }
        }).finally(() => {
            this.setState({
                sendingRequest: false
            })
        })

    }

    render() {
        return (
            <div className="container">
                {this.state.subscription ? (
                    <UserTable
                        subscription={this.state.subscription}
                    />
                ) : (
                    <SubscribeForm
                        name={this.state.name}
                        email={this.state.email}
                        changeName={this.changeName}
                        changeEmail={this.changeEmail}
                        subscribe={this.subscribe}
                        sendingRequest={this.state.sendingRequest}
                    />
                )}
            </div>
        )
    }

}

export default Root

alert.jsx

In src directory, create a new directory called services. Then create a new file alert.jsx. Cut and paste the following code:

import React from 'react';
import { toast } from 'react-toastify'

class AlertService {
    showSuccess(title, content = '') {
        toast.success(<div dangerouslySetInnerHTML={{ __html : title + '<br/>' + content }}></div>);
    }

    showError(title, content = '') {
        toast.error(<div dangerouslySetInnerHTML={{ __html : title + '<br/>' + content }}></div>);
    }
}

export const alertService = new AlertService();

SubscribeForm.jsx

In components directory, create a new file SubscribeForm.jsx:

import React from "react"

class SubscribeForm extends React.Component {

    constructor(props) {
        super(props)
    }

    render() {
        return (
            <div className="row mt-5 justify-content-center">
                <div className="col-12 col-lg-6 border border-1 p-4">
                    <form className="">
                        <div className="form-group">
                            <label className="col-form-label">Name</label>
                            <input
                                className="form-control"
                                type="text"
                                placeholder="Please input your name"
                                value={this.props.name}
                                onChange={this.props.changeName}/>
                        </div>
                        <div className="form-group">
                            <label className="col-form-label">Email</label>
                            <input
                                className="form-control"
                                type="text"
                                placeholder="Please input your email"
                                value={this.props.email}
                                onChange={this.props.changeEmail}/>
                        </div>
                        <hr className="my-4"/>
                        <div className="form-group text-right">
                            {this.props.sendingRequest ? (
                                <button type="button" className="btn btn-primary" disabled>Sending Request...</button>
                            ) : (
                                <button type="button" onClick={this.props.subscribe}
                                        className="btn btn-primary">Subscribe</button>
                            )}
                        </div>
                    </form>
                </div>
            </div>
        )
    }

}

export default SubscribeForm

UserTable.jsx

In components directory, create a new file UserTable.jsx:

import React from "react"
import { alertService } from '../services/alert'
import Axios from "axios"
import { API_HOST } from "../config"
class UserTable extends React.Component {

    constructor(props) {
        super(props)
        this.state={
            loading: true,
            users: []
        }
    }

    componentDidMount() {
        Axios.get(`${API_HOST}/users`).then(res => {
            this.setState({
                users: res.data
            })
        }).catch(e => {
            alertService.showError('Cannot get user data...')
        }).finally(() => {
            this.setState({
                loading: false
            })
        })
    }

    render() {
        return (
            <div className="row mt-5 justify-content-center">
                <div className="col-12 col-lg-8">
                    <table className="table table-hover table-striped">
                        <thead>
                            <tr>
                                <th>No</th>
                                <th>Name</th>
                                <th>Email</th>
                            </tr>
                        </thead>
                        <tbody>
                        {this.state.loading ? (
                            <tr><td>Loading...</td></tr>
                        ) : (
                            <>
                                {this.state.users.map((user, index) => {
                                    return (
                                        <tr key={index}>
                                            <thd>{index+1}</thd>
                                            <td>{user.name}</td>
                                            <td>{user.email}</td>
                                        </tr>
                                    )
                                })}
                                {!this.state.users.length && (
                                    <tr><td>Loading...</td></tr>
                                )}
                            </>
                        )}
                        </tbody>
                    </table>
                </div>
            </div>
        )
    }

}

export default UserTable

3. Let’s check it out!

First of all, ensure that MongoDB is up and running.

Next, run Rails server for backend:

$ cd bin 
$ rails server

And then run React app:

$ npm run start

Unfortunately, port 3000 is default port for both Rails and React. You can set a different port for Rails using -p switch or modify package.json to run React app on a diffrent port. But simply you can say Y to let our React run on port 3001.

When you fill the form and click Subscribe button, the page will show you a table of all subscribed users.

4. How does this work?

When a user input name and email and click “Subscribe”, Axios will send a POST request to Rails server.

Axios.post(`${API_HOST}/users`, {
    name: this.state.name,
    email: this.state.email,
})

Since we created User model by scaffold, REST Api routes for user are already set in Rails server - in config/routes.rb:

Rails.application.routes.draw do
  resources :users
end

POST requests are handled in users#create:

# POST /users
def create
  @user = User.new(user_params)

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

Since our Rails App works as API, the users_controller will respond with JSON instead of rendering erb files.

Axios will receive the JSON result and check if there is BSONObjectId to see the user is created successfully.

Axios.post(`${API_HOST}/users`, {
    name: this.state.name,
    email: this.state.email,
}).then(res => {
    if (res.data && res.data._id) {
        this.setState({
            subscription: true
        })
    } else {
        alertService.showError('Subscription failed!')
    }
})

If the user is successfully created, it will update subscription state as true. Then the Root component will render UserTable component.

After UserTable component is mounted, it sends GET request to API, which will return JSON Array of all users stored in MongoDB:

componentDidMount() {
    Axios.get(`${API_HOST}/users`).then(res => {
      this.setState({
        users: res.data
      })
    }).catch(e => {
        alertService.showError('Cannot get user data...')  
    }).finally(() => {
        this.setState({
          loading: false
        })
    })
}

If the result is successful, it will update users state and show all users stored in database:

Here is an image of MongoDB Compass showing stored users:

Useful Links

💖 💪 🙅 🚩
bravemaster619
bravemaster619

Posted on March 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related