Create a Back End API with Ruby on Rails
By Steve Carey - 7/30/2018
This Tutorial covers:Finished code: Github
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.
This is an introductory tutorial on how to build an Application Programming Interface (API) with Ruby on Rails. It assumes you are already familiar with Ruby on Rails. Essentially we are building an application based on the Ruby on Rails Model-View-Controller architecture but without the view. Instead we will provide URL endpoints that interact with a separate front-end application. In this tutorial series our front end is a React Web interface, but it could be a different library or framework like AngularJS or Vue.js or a mobile app.
Quick note on terminology. In this tutorial the terms back end, server side, and API all refer to the Rails API and can be used interchangably. The terms front end, client, user interface, and React views in the context of this tutorial all refer to the React components and can also be used mostly interchangably. Throughout this tutorial we will preface Terminal commands with a dollar sign (but don't enter the dollar sign). Code you enter in files will start with "#" and the file name at the top
Determining what type of API you need depends on what type of front end client or clients your app will interact with. In the past, APIs were intended for external web sites to access data from your site, usually read only. Then smartphones came on the scene and suddenly millions of mobile apps required access to data through APIs with both read and write capabilities. Now with JavaScript libraries and Frameworks like React, Angular, Vue and others acting as separate front-end applications, frameworks like Rails need to provide APIs just for their own front end. And even then you have the option of using the front-end application exclusively for all your views as with a Single-Page Application (SPA). Or you can use something like React for some of your front-end components and Rails views for the others. Today APIs are used for all these types of clients and more.
Some of the ways you can set up an API for these needs include:
Traditionally, if you wanted to use a front-end library like React with your Rails app you would create a completely separate front-end application. This is fine for a SPA, but if you want to use React for only some of your views how would you do it? Rails 5.1 and up ships with a gem called webpacker that allows you to integrate front-end libraries like React, Angular, and Vue directly into your Rails app. Actually, the Webpacker gem is a middleware between Rails and the popular Webpack JavaScript pre-processor and bundler. We will cover Webpacker in Integrate React in Rails with Webpacker.
The must common format for transmitting data to and from APIs is JavaScript Object Notation (JSON). JSON is not actually JavaScript. It is just a text string that is in the format of JavaScript objects, with attribute-value pairs separated by colons and surrounded by curly braces. Multiple objects are grouped in square brackets just like JavaScript arrays. JavaScript has built-in methods for converting JSON stings into JavaScript objects and JavaScript objects into JSON strings.
Your Rails API needs to provide endpoints to interact with the client. The endpoints are the URLs that the client calls with an HTTP request. Besides the URL, the request includes other information such as the HTTP method (GET, POST, PUT, PATCH, or DELETE). And for POST, PUT and PATCH requests it will include your data in the body, and the content type to indicate what format the data is in, in our case JSON. Each URL endpoint will have a corresponding controller action. That's where you program the action to take, often interacting with the database to get or set data. Your server then sends back a response to the client. If it was a GET request, the response would be the requested data in a JSON string. Your server also sends a status code back such as 200 for success, 404 for page not found, or 500 for internal server error to name a few.
Okay, let's build our Ruby on Rails API. Our application will include a database that holds articles that users can read, write, edit or delete. Rails ships with JSON API capability right out of the box. When you generate a scaffold it creates a controller with both html and JSON format. The JSON format can be used for an API. Let's use it.
mkdir rails-api-with-react; cd rails-api-with-react
- Make your application folder and change directories into it.rails new .
- Generate the app.
rails generate scaffold Article title:string content:text --no-assets --no-helper
rails db:migrate
This will generate an articles database table, an Article model, a set of RESTful routes, an articles_controller, and HTML and JBuilder views. The latter is the serializer converting your data to JSON strings.
The controller that the scaffold generated responds to both HTML and JSON formats. As a convenience, it lists the URL paths as comments above each action. The index, show, create, update, and destroy actions all allow JSON format requests. Rails treats the request as an HTML request by default unless the URL has .json appended to it.
Below is the scaffold generated controller. We'll use it as is except we need to add the line protect_from_forgery unless: -> { request.format.json? }
at the top. Alternatively, we could put this line in the application_controller.rb file so it applies to all controllers. We'll discuss this in the CORS and Security section shortly.
# app/controller/articles_controller.rb class ArticlesController < ApplicationController protect_from_forgery unless: -> { request.format.json? } # GET /articles # GET /articles.json def index @articles = Article.all end # GET /articles/1 # GET /articles/1.json def show @article = Article.find(params[:id]) end # GET /articles/new def new @article = Article.new end # GET /articles/1/edit def edit @article = Article.find(params[:id]) end # POST /articles # POST /articles.json def create @article = Article.new(article_params) respond_to do |format| if @article.save format.html { redirect_to @article, notice: 'Article was successfully created.' } format.json { render :show, status: :created, location: @article } else format.html { render :new } format.json { render json: @article.errors, status: :unprocessable_entity } end end end # PATCH/PUT /articles/1 # PATCH/PUT /articles/1.json def update @article = Article.find(params[:id]) respond_to do |format| if @article.update(article_params) format.html { redirect_to @article, notice: 'Article was successfully updated.' } format.json { render :show, status: :ok, location: @article } else format.html { render :edit } format.json { render json: @article.errors, status: :unprocessable_entity } end end end # DELETE /articles/1 # DELETE /articles/1.json def destroy @article = Article.find(params[:id]) @article.destroy respond_to do |format| format.html { redirect_to articles_url, notice: 'Article was successfully destroyed.' } format.json { head :no_content } end end private def article_params params.require(:article).permit(:title, :content) end end
The index, show, create, update, and delete actions are all API end points that respond to JSON requests.
The index and show actions respond to GET requests by pulling article records from the articles database table and rendering them. In Rails the render for GET requests is implied but you could add render :index
and render :show
to the index and show actions if you wanted to be explicit. For JSON requests, when using the JBuilder gem, Rails will render JSON views through the JBuilder view files. Below are the ones auto generated with the scaffold refactored to not use a partial. You can filter which attributes you want to send back and indicate the format to use. Rails then converts the data to a JSON string and transmits it back to the requester with the response.
# app/views/articles/index.json.jbuilder json.array! @articles do |article| json.extract! article, :id, :title, :content, :created_at, :updated_at json.url article_url(article, format: :json) end
# app/views/articles/show.json.jbuilder json.extract! @article, :id, :title, :content, :created_at, :updated_at json.url article_url(@article, format: :json)
A POST request is handled by the create action. Rails converts the request body from a JSON string to a Ruby object and attempts to save it to the database after checking the params in the article_params helper method. If it is saved successfully, Rails will render the show method. Otherwise it returns the error messages with a status of :unprocessable_entity which corresponds to status code 422.
PUT and PATCH requests are handled similarly as a POST request. PUT is intended to update an entire record while PATCH is intended to update only parts of a record. Rails maps them both to the "update" action so it doesn't matter which you use. However, since Rails itself is responsible for setting the updated_at attribute and created_at will not be changed, it's technically more correct to use the PATCH method.
DELETE requests are triggered regardless of format (HTML or JSON). However, you need to specify the JSON format to get the right response, which in this case is no content at all. Otherwise it will be a redirect to the articles_url which will return a response of all the HTML from the index page.
# 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
We should be able to view our articles in both HTML and JSON formats.
rails server -p 3001
- Start the server on port 3001GET requests can be made to your API but POST, PUT, and DELETE requests will return an error code of 422 Unprocessable Entity. Rails uses the same-origin security policy which forbids "cross-domain" requests by default. Cross-domain requests are requests coming from a different domain than the server, so in this case from any domain other than localhost:3001. A web application can use the Cross-Origin Resource Sharing (CORS) system to allow AJAX requests like POST, PUT/PATCH, and DELETE from a separate domain. It uses information passed with HTTP headers.
To enable CORS we need to add the rack-cors gem to our Gemfile. This is CORS middleware for Rack compatible web applications like Rails. In the gemfile add:
# Gemfile gem 'rack-cors'
Then install it.
bundle install
# config/application.rb module ArticlesAppWithApi class Application < Rails::Application ... config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:3000' #1 resource '*', :headers => :any, :methods => [:get, :post, :put, :patch, :delete, :options] # 2 end end end end
1) Origins: whitelists what domains are allowed to make restricted requests. For any origin use '*'. Otherwise list the specific domains such as 'http://localhost:3000' or http://www.example.com.
2) resource: specify which resource or resources the access applies to. '*' means it applies to the whole site. You can also specify which HTTP methods are allowed.
In addition to allowing CORS access above, in the controller we disabled a built-in Rails Cross Site Request Forgery protection feature when we added this statement: protect_from_forgery unless: -> { request.format.json? }
.
With this security feature, post, patch/put, and delete requests from rails forms or pages include an authenticity token. In a standard Rails app those are submitted as a hidden field with the form. But if we are using a form submitted to our API from a React front end, Rails won't generate the security token. So we need to disable that security feature for JSON requests.
So how do you restrict who can make write requests to the API? We already are restricting what domain they can come from but what about restricting what users can access the API endpoints? There are methods to do that such as using JSON Web Tokens. We will cover that in Rails React and JWTs.
We're not done with our API until we know all the endpoints we created work. I'll discuss two ways we can do this. First is using Curl.
Curl is a command line utility that lets you make HTTP requests from the Terminal instead of from a web page. Curl comes preloaded with Macs. For Windows you may need to install it, but it's a handy tool sometimes. Let's make a POST request by entering the below Curl command. The -H option is for the header which is where we tell the receiver that the data we are sending is in JSON format. The -X option specifies the request method, POST in this case. The -d option is where we put our data, so you won't use the -d option with GET or DELETE requests. And of course we include the URL.
rails s -p 3001
Make sure you start the server if it's not already runningcurl -H "Content-Type: application/json" -X POST -d '{"title":"Learn JavaScript","content":"Lorem ipsum."}' http://localhost:3001/articles.json
If it posted the server should send a response back with the new Article data in JSON format. It will have the title and content fields as you submitted, but it will also have id, created_at, and updated_at fields. You can also make a Curl GET request with the new ID:
curl -X GET http://localhost:3001/articles/4.json
- change the id to the correct one if it's not 4.
Now let's update this record with a PATCH request:
curl -H "Content-Type: application/json" -X PATCH -d '{"title":"Learn ES6","content":"Lorem ipsum."}' http://localhost:3001/articles/4.json
It returns a JSON string of the saved updated article data. Now, let's delete it:
curl -X DELETE http://localhost:3001/articles/4.json
In our controller destroy action we specified to return no content, so it won't give you confirmation that it was deleted. But, if you try to do a GET request on it will throw you an error.
If you don't like the cryptic feel of sending HTTP requests from the command line, there is a popular free desktop application called Postman that lets you do the same thing from a GUI interface. You can download it from getpostman.com. The tool is fairly intuitive but you will need to play around with it a bit to get comfortable with it. If you plan on building a lot of APIs Postman is an essential tool. Here are the CRUD actions hitting our API's endpoints:
You now have a working Rails API. You can move on to the next section of this series React Front End. Or you can continue on and explore more about Rails APIs. We will briefly cover three more topics, including rebuilding our Rails app as an API only application in under five minutes.
Serialization in the context of a web application is the process of converting data objects into a format like a JSON string that can transmitted across a network or the internet and deserialized back into a data object when it is received. We used the JBuilder gem as our serializer above, but there are other options. A popular one is a gem called Active Model Serializers (AMS). For reference, AMS docs are on github.com. While JBuilder approached serialization through it's view files, AMS approaches it like a model.
Before going into the details of AMS it is worth mentioning that in February 2018 Netfix released a similar gem called fast_jsonapi that is supposed to be very fast. The concept is similar to AMS so we'll continue with the instructions for AMS since it's tried and true. You can read more about fast_jsonapi on their Github page.
First, install the AMS gem. You can uninstall the JBuilder gem at the same time if don't use it.
# Gemfile gem 'active_model_serializers'
bundle install
Then when you generate a resource or scaffold, the generator will also create an app/serializers/article_serializer.rb file. Or if you already created the article resource you can generate the serializer separately with or without specifying the attributes:
rails g serializer article
rails g serializer article title content
This is the file it generates:
# app/serializers/article_serializer.rb class ArticleSerializer < ActiveModel::Serializer attributes :id, :title, :content end
Your controller would look something like this. In this case we are assuming you are only returning JSON format, not HTML and JSON both.
# app/controllers/articles_controller.rb class ArticlesController < ApplicationController protect_from_forgery unless: -> { request.format.json? } def index @articles = Article.all render json: @articles end def show @article = Article.find(params[:id]) render json: @article end def create @article = Article.new(article_params) if @article.save render json: @article, status: :created else render json: @article.errors, status: :unprocessable_entity end end def update @article = Article.find(params[:id]) if @article.update(article_params) render json: @article, status: :ok else render json: @article.errors, status: :unprocessable_entity end end def destroy @article = Article.find(params[:id]) @article.destroy head :no_content end private def article_params params.require(:article).permit(:title, :content) end end
And you no longer need the JBuilder view files. So you can delete them.
That's it. Your API using AMS should now be working. If you start the server on port 3001 and in your browser go to localhost:3001/articles, you should get the JSON output with or without appending .json to the URL.
Traditional web applications that also provide an API often add an "api" namespace to the URL to distinguish it from a regular URL. So instead of www.example.com/articles, the API URL would be www.example.com/api/articles. And if the API serves a mobile app, it is common to add versions to the api namespace so that changes won't break existing downloaded mobile apps. Something like www.example.com/api/v1/articles.
There are a couple of approaches to add such a namespace to your app. First is to add a scope to the route. Something like this:
# config/routes.rb scope '/api' do resources :articles end
Then the URL will include the api namespace, but the controller will not. It will just be the same controller we generated before.
The second option is to add a namespace to the route, controller, and views (if any). This makes more sense if you are using versioning since you will then have multiple controllers.
# config/routes.rb namespace :api do namespace :v1 do resources :articles end end
The controller class name would look like this.
# app/controllers/api/v1/articles_controller.rb class Api::V1::ArticlesController < ApplicationController ... end
Beginning with Rails 5.0 you have the option of generating an API only application instead of the traditional Rails app. 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.
We can build the same Articles app as an API only app in less than five minutes. Ready. Go:
mkdir appname; cd appname
To generate an API-only app just add the --api flag
rails new . --api
With an API only app you don't need to use the jbuilder or active_model_serializers gems. Rails serializes the JSON format automatically. You can generate a scaffold and it will automatically create a controller that renders JSON. And you don't need to add the protect_from_forgery unless: -> { request.format.json? }
statement to your controller.
rails generate scaffold Article title:string content:text
rails db:migrate
# 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 the rack-cors gem to prevent Rails from blocking cross browser access, since our front end and back end will be running on different ports.
# Gemfile gem 'rack-cors'
bundle install
# config/application.rb module ArticlesAppWithApi class Application < Rails::Application ... config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:3000' resource '*', :headers => :any, :methods => [:get, :post, :put, :patch, :delete, :options] end end end end
Add an "api" namespace to the routes. This will avoid potential route collisions between your back-end api and your front-end react routes.
# config/routes.rb scope '/api' do resources :articles end
rails server -p 3001
Done!
Okay, we have a working API. Or two. Now let's build a separate front-end React application to interact with it. On to the next section.