DEV Community

Kattya Cuevas
Kattya Cuevas

Posted on • Edited on

Building and Consuming a JSON API with Rails and React

When I thought about how to build an API, I started to search for what is the "best way" to do it. I found that exists specifications to build an API, you can found it here https://jsonapi.org. There, you would found a list of "rules" which we have to follow about how to send and receive data in an API.

The next doubt I had, after knowing the "best way" to build an API, is how am I going to build that API with all those rules? It looks so much work to do. Well... That's not true! In Rails, it's easy with a gem called jsonapi-resources.

In this project, the frontend will be done with React. The last version of Rails (v.6.0.0), Rails comes with Webpacker integrated (gem to handle the integration Rails + Webpack). It will make easier for us to use React. 🙌

Consume the data from our API with React, it's not hard. But, formatting the data to send to the API could be complex. There is another library to do this! Also, this library is going to help you to validate the form data. This library is Formik.

Let's start!

Versions of the tools we are going to use:

  • Ruby 2.6.3
  • Rails 6.0.0
  • Yarn 1.17.3

Setup Base Project

To create a new project with rails, we need to use the rails new command with the project name at the end.

We could also add some additional options. In this case, we will use --database=postgresql to use PostgreSQL as our database, --skip-turbolinks to avoid using turbolinks because we will handle routing in the frontend, and --webpack=react to make Rails generate the configuration for us to use React.js.

$ rails new my-app --database=postgresql --skip-turbolinks --webpack=react
Enter fullscreen mode Exit fullscreen mode

Now, we're going to add a model called Post with 2 attributes: title and body. title is a string and body is a text. In Rails, the model represents the database tables. We can generate it with the rails generate model command followed by the model name with the attributes. The attributes should be separated by spaces and has the name and the type divided by :, like title:string. If we don't specify the type of the attribute, Rails will default to the type string.

The command generates a file with the model definition and a migration file that specifies the change to be made in the database, in this case, is the creation of the new table.

$ rails generate model Post title body:text
$ rails db:create
$ rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Note: We could also use rails g which is an alias of rails generate.

The rails db:create command creates the database of the project and the rails db:migrate command runs all the pending migrations since this is a new project it will run every migration.

We could add some seed data. To do it, we have to open the db/seeds.rb file and add the following lines:

Post.create(title: "Post 1", body: "My first Post")
Post.create(title: "Post 2", body: "My second Post")
Enter fullscreen mode Exit fullscreen mode

And to populate the database with our seed data, we need to run the command:

$ rails db:seed
Enter fullscreen mode Exit fullscreen mode

In Rails projects, we should define the main route of the application this one is going to handle the path /. Go to config/routes.rb to define it and inside of the block Rails.application.routes.draw, add:

root to: "home#index"

get "*path", to: "home#index", constraints: { format: "html" }
Enter fullscreen mode Exit fullscreen mode

Note: The routes are defined as "home#index", this means the controller which is going to control the behavior is HomeController and the specified action in the controller is index.

We have to create the HomeController. First, let's create the home_controller.rb file in app/controllers folder. Inside, add the index action:

class HomeController < ApplicationController
  def index; end
end
Enter fullscreen mode Exit fullscreen mode

Every action renders a view, in this case using HTML. We need to create the view in app/views/home folder and name it index.html.erb. In this file, we have to render the script to load our React app.

<%= javascript_pack_tag 'posts' %>
Enter fullscreen mode Exit fullscreen mode

The helper javascript_pack_tag will generate the following script tag:

<script src="/packs/js/posts-a447c92837fa3b701129.js"></script>
Enter fullscreen mode Exit fullscreen mode

Note: The name of the pack is generated with a hash added at the end to let us cached it for a long time.

This script will load the pack posts.jsx. We have to create that pack in the app/javascript/packs folder:

import React from "react";
import ReactDOM from "react-dom";
import App from "components/App";

document.addEventListener("DOMContentLoaded", () => {
  ReactDOM.render(
    <App />,
    document.body.appendChild(document.createElement("div"))
  );
});
Enter fullscreen mode Exit fullscreen mode

We are going to use @reach/router to handle the routes in our React app. To install it, run:

$ yarn add @reach/router
Enter fullscreen mode Exit fullscreen mode

Let's create the component App.js in app/javascript/components folder. We will use this component to manage the routes.

import React from "react";
import { Router } from "@reach/router";
import PostList from "./PostList";

function App() {
  return (
    <Router>
      <PostList path="/" />
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Here we will create our first route /, which is going to render the PostList component.

Now we are going to create the component PostList.js in app/javascript/components folder.

import React from "react";

function PostList() {
  return <div>Hello from my React App inside my Rails App!</div>;
}

export default PostList;
Enter fullscreen mode Exit fullscreen mode

Inside we are going to render a div to test our React App.

Start the Server

We need to install foreman to run the React and Rails apps at the same time. We can install it with the command:

$ gem install foreman
Enter fullscreen mode Exit fullscreen mode

We should create a Procfile.dev file in the root of the project. Inside it, add:

web: bundle exec rails s
webpacker: ./bin/webpack-dev-server
Enter fullscreen mode Exit fullscreen mode

To start the server, we need to run the command:

$ foreman start -f Procfile.dev
Enter fullscreen mode Exit fullscreen mode

Create the API

To create our API following the JSON:API specification, we are going to use the gem jsonapi-resources. To use it, we have to add it to the Gemfile and install it running bundle install.

JSONAPI::Resources provides helper methods to generate correct routes. We'll add the routes for API in config/routes.rb, before get "*path":

namespace :api do
  jsonapi_resources :posts
end
Enter fullscreen mode Exit fullscreen mode

Note: namespace :api is going to generate routes with /api before the route. Eg. /api/posts.

We're going to create the ApiController, to extend the controller from the ActionController::API module of Rails, and also we're going to include the JSONAPI::ActsAsResourceController from JSONAPI::Resources.

class ApiController < ActionController::API
  include JSONAPI::ActsAsResourceController
end
Enter fullscreen mode Exit fullscreen mode

Now we need to create the PostsController. We should create it inside a folder named api because our routes config is going to search for an Api::PostsController class.

class Api::PostsController < ApiController
end
Enter fullscreen mode Exit fullscreen mode

jsonapi_resources :posts require a PostResource class defined. We have to create PostResource in app/resources/api/post_resource.rb.

class Api::PostResource < JSONAPI::Resource
  attributes :title, :body
end
Enter fullscreen mode Exit fullscreen mode

Here, we define the attributes and relationships we want to show as part of the resource.

To see how our response looks like, go to localhost:5000/api/posts.

Consume the API

We will make the React app consume our API. First, let's only read the data. Edit the PostList component to fetch the list of posts.

import React, { useEffect, useState } from "react";

function PostList() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const requestPosts = async () => {
      const response = await fetch("/api/posts");
      const { data } = await response.json();
      setPosts(data);
    };
    requestPosts();
  }, []);

  return posts.map(post => <div>{post.attributes.title}</div>);
}

export default PostList;
Enter fullscreen mode Exit fullscreen mode

Inside a useEffect, we will do the fetch to /api/posts and save the response in the state of the component.

Now, let's create the form to add more posts. But first, we have to add formik as a dependency in the React app.

$ yarn add formik
Enter fullscreen mode Exit fullscreen mode

We are going to create a new component to show the form, let's call it AddPost.js. In this component, we are going to make a POST method to /api/posts with the correct format of data to create a new post.

import React from "react";
import { navigate } from "@reach/router";
import { Formik, Field, Form } from "formik";

function AddPost() {
  const handleSubmit = values => {
    const requestPosts = async () => {
      // We get the CSRF token generated by Rails to send it
      // as a header in the request to create a new post.
      // This is needed because with this token, Rails is going to
      // recognize the request as a valid request
      const csrfToken = document.querySelector("meta[name=csrf-token]").content;
      const response = await fetch("/api/posts", {
        method: "POST",
        credentials: "include",
        headers: {
          "Content-Type": "application/vnd.api+json",
          "X-CSRF-Token": csrfToken
        },
        body: JSON.stringify({ data: values })
      });
      if (response.status === 201) {
        navigate("/");
      }
    };
    requestPosts();
  };

  return (
    <div>
      <h2>Add your post</h2>
      <Formik
        initialValues={{
          type: "posts",
          attributes: {
            title: "",
            body: ""
          }
        }}
        onSubmit={handleSubmit}
        render={() => (
          <Form>
            <Field type="text" name="attributes.title" />
            <Field type="text" name="attributes.body" />

            <button type="submit">Create</button>
          </Form>
        )}
      />
    </div>
  );
}

export default AddPost;
Enter fullscreen mode Exit fullscreen mode

Finally, we need to add the route /add in our React app.

import React from "react";
import { Router } from "@reach/router";
import PostList from "./PostList";
import AddPost from "./AddPost";

function App() {
  return (
    <Router>
      <PostList path="/" />
      <AddPost path="/add" />
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

If we go to localhost:5000/add, we will see the form. If we fill the fields and click on Submit, it will create a new post and will navigate automatically to localhost:5000/, where we will see our new post as part of the list.

If we reload the page, the React app will fetch our post again with the new post we just created.

That's how we can create an application with Rails + React, following the JSON:API spec.

I would love any feedback about the post or the libraries used here. ❤️

Top comments (11)

Collapse
 
kacalamaka profile image
Kacalamaka

Hi, I have an error when I run the command "foreman start -f Procfile.dev"

The error message are written below:

D:/CVWO/project/config/routes.rb:5:in block (2 levels) in <top (required)>': undefined methodjsonapi_resources' for #ActionDispatch::Routing::Mapper:0x... (NoMethodError)

Can somebody help me resolve this isssues?
I try look up for solutions on the internet, but there weren't any. I am also green to react-rails.

Collapse
 
athuyaoo profile image
Thuya

Did you install the gem jsonapi_resources?

Collapse
 
mikenath223 profile image
Michgolden Ukeje

Hi, Really love your post. But please when you wrote this:

This script will load the pack posts.jsx. We have to create that pack in the app/javascript/packs folder:

Where do i fix this code below:

import React from "react";
import ReactDOM from "react-dom";
import App from "components/App";

document.addEventListener("DOMContentLoaded", () => {
ReactDOM.render(
,
document.body.appendChild(document.createElement("div"))
);
});

Collapse
 
nicktohzyu profile image
nicktohzyu • Edited

You wrote

We're going to create the ApiController, to extend the controller from the ActionController::API module of Rails, and also we're going to include the JSONAPI::ActsAsResourceController from JSONAPI::Resources.

class ApiController < ActionController::API
include JSONAPI::ActsAsResourceController
end

Which file does this go in, and what is the path to that file? (am total beginner)

Collapse
 
athuyaoo profile image
Thuya

This should be in the controllers folder.

Collapse
 
thiagocardoso1988 profile image
Thiago Cardoso

any light on how to fix this error on Rails 6.1:

uninitialized constant ActionController::ForceSSL (NameError)

this happens when I try to spin up the server, after I installed jsonapi

Collapse
 
thiagocardoso1988 profile image
Thiago Cardoso

I saw an issue here, but without resolution :( github.com/cerebris/jsonapi-resour...

Collapse
 
kattyacuevas profile image
Kattya Cuevas

Hi Thiago! I've just seen the issue. There are other libraries to implement JSON API spec with rails. You can see them here: jsonapi.org/implementations/#serve...

Collapse
 
johnfajardo profile image
John Fajardo • Edited

Why use a gem for json instead of using the --api flag when creating the new rails project?

Collapse
 
kattyacuevas profile image
Kattya Cuevas

It's because I am building a web application which included an API inside. With the --api flag, I lose the initial configuration to render HTML views.

I did that instead of having 2 separated applications because I prefer to handle the routes with Rails, and after that, load the correct React application.

Collapse
 
lavaldi profile image
Claudia Valdivieso

I'll finally learn JSON API 🙌 Thanks Kattya!