Note: the slide version of this workshop is available here.
-
This workshop is going to be breaking down the steps of how turn a Rails app into a simple API.
-
We'll be going under the assumption, we've built previous Rails apps before.
-
I suggest to follow the steps to create a new app in the README instead of trying to clone it.
We will build a Rails application that acts solely as an API. Instead of displaying HTML pages, it'll render JSON.
In this separate workshop, we'll build a React application to consume this API.
rails new rails-cafe-api -d postgresql --apiWith the --api flag, there are 3 main differences:
- Configure your application to start with a more limited set of middleware than normal. Specifically, it will not include any middleware primarily useful for browser applications (like cookies support) by default.
- Make
ApplicationControllerinherit fromActionController::APIinstead ofActionController::Base. As with middleware, this will leave out any Action Controller modules that provide functionalities primarily used by browser applications. - Configure the generators to skip generating views, helpers, and assets when you generate a new resource.
You can read more about the changes in the official documentation.
Feel free to change the respository name:
gh repo create rails-cafe-api --public --source=.We're going to keep this tutorial simple. We'll just have a cafe model. Based around this information, which we'll be seeding into our app eventually.
title: stringaddress: stringpicture: string (⚠️ We're not using ActiveStorage for simplicity sake).hours: hash (⚠️ see how to create this below)criteria: array (⚠️ see how to create this below)
Create the DB before the model
rails db:create- The pluralization is built in to handle things like
person=>peopleandsky=>skiesetc. - But when we generate a
cafemodel in Rails, it creates a table calledcaves.... which is obviously not what we want. Here is a StackOverflow answer on how to fix it
So let's go into our config/initializers/inflections.rb and add this:
ActiveSupport::Inflector.inflections do |inflect|
inflect.plural "cafe", "cafes"
endThen create the model
rails g model cafe title:string address:string picture:string hours:jsonb criteria:stringYou'll notice that when we create the hours hash, we're actually using a jsonb type.
You can see how this works in the official documentaion.
And also when we create the criteria array, we're actually specifying a string at first. But we'll have to update the migration (before we migrate) to indicate we're using an array:
t.string :criteria, array: trueYou can see how this works in the official documentaion.
Then run the migration and our DB should be ready to go.
rails db:migrateIt's up to you at this point, but we'll add three validations on the cafe model so that we need at least a title and address in order to create one. And also a uniqueness so that the same cafe at the same address can't be recreated.
# cafe.rb
validates :title, presence: true, uniqueness: { scope: :address }
validates :address, presence: trueWe were basing our data on around this information already so we've got a JSON that we can use in our seeds.
- We'll open that link using
open-uri - Turn the JSON result into a Ruby array
- Iterate over the array and create an instance of a
cafefor each hash in the array.
The point of this workshop is not how to seed the DB, so the code is already set in our db/seeds.rb file.
require 'open-uri'
puts "Removing all cafes from the DB..."
Cafe.destroy_all
puts "Getting the cafes from the JSON..."
seed_url = 'https://gist.githubusercontent.com/yannklein/5d8f9acb1c22549a4ede848712ed651a/raw/3daec24bcd833f0dd3bcc8cee8616a731afd1f37/cafe.json'
# Making an HTTP request to get back the JSON data
json_cafes = URI.open(seed_url).read
# Converting the JSON data into a ruby object (this case an array)
cafes = JSON.parse(json_cafes)
# iterate over the array of hashes to create instances of cafes
cafes.each do |cafe_hash|
puts "Creating #{cafe_hash['title']}..."
Cafe.create!(
title: cafe_hash['title'],
address: cafe_hash['address'],
picture: cafe_hash['picture'],
criteria: cafe_hash['criteria'],
hours: cafe_hash['hours']
)
end
puts "... created #{Cafe.count} cafes! ☕️"Run the seeds rails db:seed and have a look in the rails console to see our cafes.
If this is your first time building an API the routing is going to look a bit different from normal CRUD routes inside of a Rails app. We're going to add the word api in our route but also version it. So that if we end up updating the API, we dont have to break the old flow for apps relying on it. We can just shift to the second version.
So our user stories with routes:
- I can see all cafes
get '/api/v1/cafes'
- I can create a cafe
post '/api/v1/cafes'
How to namespace in our routes.rb
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :cafes, only: [ :index, :create ]
end
endHere we're also saying to expect json (since it's an API) instead of the normal HTML flow.
Now we need to create the cafes_controller but we're going to create one specifically for v1 of our api. This gives us flexibility later on to create a separate controller for the next version.
To generate
rails g controller api/v1/cafesThis creates our controller. But also, it creates a folder called api inside of our controllers folder. Then another one called v1 inside of that.
Let's start with the index. It will follow normal Rails CRUD to pull all of the cafes from the DB.
def index
@cafes = Cafe.all
endIf we allow users to search for cafes by their title in our app, we can add that into our action as well:
def index
if params[:title].present?
@cafes = Cafe.where('title ILIKE ?', "%#{params[:title]}%")
else
@cafes = Cafe.all
end
endBUT, this is the biggest difference from building an API compared to one with HTML views. Instead of rendering HTML, we're going to render JSON.
def index
if params[:title].present?
@cafes = Cafe.where('title ILIKE ?', "%#{params[:title]}%")
else
@cafes = Cafe.all
end
# Putting the most recently created cafes first
render json: @cafes.order(created_at: :desc)
endNow let's test out the endpoint. If we want to see our routes, we can check with rails routes.
This tells us to trigger our cafes#index action, we need to type /api/v1/cafes after our localhost.
Launch a rails s and check it out in the browser. You should be seeing JSON (intead of HTML).
POST request instead of a GET. And we don't have an HTML form either. The easiest way to test this endpoint would be to use Postman. In Postman, we'll need to make sure we're sending a POST to the correct address, but also sending the correct params.
We'll want our request to look like this:
Or just the request code:
{
"cafe": {
"title": "Le Wagon Tokyo",
"address": "2-11-3 Meguro, Meguro City, Tokyo 153-0063",
"picture": "https://www-img.lewagon.com/wtXjAOJx9hLKEFC89PRyR9mSCnBOoLcerKkhWp-2OTE/rs:fill:640:800/plain/s3://wagon-www/x385htxbnf0kam1yoso5y2rqlxuo",
"criteria": ["Stable Wi-Fi", "Power sockets", "Coffee", "Food"],
"hours": {
"Mon": ["10:30 – 18:00"],
"Tue": ["10:30 – 18:00"],
"Wed": ["10:30 – 18:00"],
"Thu": ["10:30 – 18:00"],
"Fri": ["10:30 – 18:00"],
"Sat": ["10:30 – 18:00"]
}
}
}Our create action is going to look exactly like a normal CRUD create action, except for when an error occurs. Instead of rerendering a form like we would in HTML, we'll respond back with the error inside of the JSON response:
render json: { error: @cafe.errors.messages }, status: :unprocessable_entitySo our full create controller action will look something like:
def create
@cafe = Cafe.new(cafe_params)
if @cafe.save
render json: @cafe, status: :created
else
render json: { error: @cafe.errors.messages }, status: :unprocessable_entity
end
end
private
def cafe_params
params.require(:cafe).permit(:title, :address, :picture, hours: {}, criteria: [])
endCORS == Cross-origin resource sharing (CORS) A nice explanation can be found in this article. In summary:
CORS is an HTTP-header based security mechanism that defines who’s allowed to interact with your API. CORS is built into all modern web browsers, so in this case the “client” is a front-end of the application.
In the most simple scenario, CORS will block all requests from a different origin than your API. “Origin” in this case is the combination of protocol, domain, and port. If any of these three will be different between the front end and your Rails application, then CORS won’t allow the client to connect to the API.
So, for example, if your front end is running at https://example.com:443 and your Rails application is running at https://example.com:3000, then CORS will block the connections from the front end to the Rails API. CORS will do so even if they both run on the same server.
So the TL;DR is that we have to enable our front-end to access our back-end in 2 steps:
- Uncomment
gem "rack-cors"in the GEMFILE, thenbundle install - Go to
config/initializers/cors.rband specify from which URL (and which actions) that you are willing to accept requests
For example:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://example.com:80'
resource '/orders',
:headers => :any,
:methods => [:post]
end
endOr to just blindly allow all (only for now)
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :patch, :put]
end
end- Adding users and Pundit 👉 Le Wagon student tutorial
- Adding ActiveStorage and Cloudinary 👉 Setup instructions
- Using JBuilder for JSON views 👉 jbuilder docs / jbuilder example
- Writing tests 👉 Setup RSpec, Video part 1 and part 2
- Test Examples 👉 Controller Example / Model Example
Without knowing the endpoints or having our React app up and running, we have no way to interact with this app. So we can build a simple interface so that someone can play around with our API.

rails g controller pages homeLet's generate this controller (plus the view), but we need to be careful here on what it's creating. We need to update the routes and controller.
You might notice that the new controller inheriting from ApplicationController which is inheriting from ActionController::API. It's using the API module because we created our app with the --api flag. This limits our app to only API functionality so we can change it back to the normal Rails flow. So, our controller should inherit from ActionController::Base
class PagesController < ActionController::Base
def home
end
endThe generator also created a route get "pages/home", which actually isn't useful to us at all. Let's go into our config/routes.rb file and change that to root to: pages#home instead.
Since we used the --api flag when creating the app, we don't have the typical views/layouts/application.html.erb file anymore. Our app wasn't expecting any HTML views. So for our home.html.erb page, We'll have to add a full HTML setup. You can use the one in this tutorial as a starting point.
We've "tagged" our cafes with certain criteria ie: wifi, outlets, coffee etc.
Let's create an end-point for our front-end so that we can display all of these criteria.
Add in a criteria index inside our our namespaced routes.
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :cafes, only: [ :index, :create ]
resources :criteria, only: [ :index ]
end
endGenerate controller
rails g controller api/v1/criteriaWe don't actually have a criteria model so we're going to pull all of the criteria from our cafes using the .pluck and .flatten methods. Then make sure we're not duplicating any using the .uniq method:
def index
@criteria = Cafe.pluck(:criteria).flatten.uniq
render json: @criteria
endWe can test it out by visiting /api/v1/criteria in the browser which should return a JSON array of our criteria.




