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
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
Note: We could also use
rails g
which is an alias ofrails 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")
And to populate the database with our seed data, we need to run the command:
$ rails db:seed
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" }
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 isindex
.
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
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' %>
The helper javascript_pack_tag
will generate the following script tag:
<script src="/packs/js/posts-a447c92837fa3b701129.js"></script>
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"))
);
});
We are going to use @reach/router
to handle the routes in our React app. To install it, run:
$ yarn add @reach/router
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;
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;
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
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
To start the server, we need to run the command:
$ foreman start -f Procfile.dev
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
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
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
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
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;
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
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;
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;
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)
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 method
jsonapi_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.
Did you install the gem jsonapi_resources?
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"))
);
});
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)
This should be in the controllers folder.
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
I saw an issue here, but without resolution :( github.com/cerebris/jsonapi-resour...
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...
Why use a gem for json instead of using the --api flag when creating the new rails project?
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.
I'll finally learn JSON API 🙌 Thanks Kattya!