Welcome to a new ongoing tutorial mini-series dedicated to building an API-driven Ruby on Rails application.
The goal of this series is to give some perspective of what it's like building a backend API that can talk to multiple applications no matter the platform.
I plan to discuss topics like returning data to a client, routing, versioning, authentication, and more.
Consider this series a work in progress. If you have questions/suggestions on what I should cover please feel free to comment below or reach out to me.
Part 1
Part 2
Part 1 Notes
In part one, I kick off the new application in --api
mode which is new to Rails since version 5.2. This mode removes all the view related components on a new rails app when running rails new
on the command line.
You can kick off the app by running:
rails new band_api --api
This generates a new app that assumes all your data will be returned as JSON
data by default. It's worth noting that you can display both HTML and JSON data by default in a vanilla Rails app. The API mode just cuts down on the cruft should you not need HTML responses. You might use the API mode if your frontend is entirely built with React or similar.
Most of the time I like to mix and match here. I'll keep most of a Ruby on Rails application vanilla with sprinkles of more javascript driven components in the app. You can return JSON for those specific components and HTML everywhere else. This allows you to not re-invent the wheel so much in my opinion.
Creating a resource
This example API will be about Bands, band members, and albums. To start we need a band resource.
rails g scaffold Band name:string
The band model simply needs a name column for now.
While we are at it you can create a Member
resource that associates itself to the Band
model
rails g model Member name:string band:references
A member will have a band_id
and name
column. The band ID is how we associate a member to a band.
rails db:migrate
With that out of the way, we can ensure our models are set up correctly.
# app/models/band.rb
class Band < ApplicationRecord
has_many :members
end
# app/models/member.rb
class Member < ApplicationRecord
belongs_to :band
end
Because we scaffolded the Band
resource we have controllers in the app/controllers
directory automatically.
# app/controllers/bands_controller.rb
class BandsController < ApplicationController
before_action :set_band, only: [:show, :update, :destroy]
# GET /bands
def index
@bands = Band.all
render json: @bands
end
# GET /bands/1
def show
render json: @band
end
# POST /bands
def create
@band = Band.new(band_params)
if @band.save
render json: @band, status: :created, location: @band
else
render json: @band.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /bands/1
def update
if @band.update(band_params)
render json: @band
else
render json: @band.errors, status: :unprocessable_entity
end
end
# DELETE /bands/1
def destroy
@band.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_band
@band = Band.find(params[:id])
end
# Only allow a list of trusted parameters through.
def band_params
params.require(:band).permit(:name)
end
end
Notice how clean it is since we are just returning JSON data only.
On the index
action we query for Band.all
and assign it to an instance variable called @bands
. That will automatically return a JOSN
object containing all bands.
First, you need to create some!
rails console
Band.create(name: "AC/DC")
Band.create(name: "The Beatles")
... # add more here if you want
With some data in the app you should be able to boot up your server and visit the url to see JSON data in your browser. I used Firefox which returns JSON automatically.
rails server
Visit localhost:3000/bands/
Returning JSON
Rails returns JSON by default if you want it to. In a default Rails app without --api
mode enabled you can leverage jbuilder which is a really awesome DSL (domain specific language) for generating JSON data while writing Ruby code. It's very simple to write and makes a lot of sense to use if you're not working on a backend-only application.
Sometimes you want more control in what returns. This might be required for generating rich relationships between models for example. Getting that sophisticated with your JSON might be best left for some third-party gems like active-model-serializers.
There are more modern approaches out in the wild for handling this but I find this gem to be pretty straight-forward to the newcomer.
bundle add active_model_serializers
With the gem add to your app you now have access to a new generator type called serializer
. Think of this as almost another model in your app that allows you to intercept what data returns in your app without writing JSON directly.
rails generate serializer band
This creates a new folder in app/
called serializers
and also adds a file called band_serializer.rb
.
That file looks like this upon first creating it.
class BandSerializer < ActiveModel::Serializer
attributes :id
end
In our controller, we are returning every column at the moment. This serializer can help us dictate what we return as well as help use return members associated with bands in a sub-object.
# app/serializers/band_serializer.rb
class BandSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :members
end
To return real members you'll need to create some data.
rails console
Member.create(name: "Angus Young", band_id: 1)
If you recall the bands you created previously, here is where you need to assign their IDs manually when creating a new member. You can always return a list of the bands in your database by running.
rails c
Band.all
Take note of each Object's id
column when you are creating members for each Band.
With Member data we now need a serializer for members much like bands
rails generate serializer Member
# app/serializers/member_serializer.rb
class MemberSerializer < ActiveModel::Serializer
attributes :id, :name
belongs_to :band
end
Now revisiting localhost:3000/bands should return both band and member data in rich JSON format!
Scaling
Sometimes JSON data can get out of hand pretty fast. This is especially true in a team setting. With this in mind there are protocols/specifications that exist which allow you to return data in a agreed upon style.
JSON API is an example of one of these specifications. Below is an example copied from their website for context:
{
"links": {
"self": "http://example.com/articles",
"next": "http://example.com/articles?page[offset]=2",
"last": "http://example.com/articles?page[offset]=10"
},
"data": [
{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/articles/1/relationships/author",
"related": "http://example.com/articles/1/author"
},
"data": { "type": "people", "id": "9" }
},
"comments": {
"links": {
"self": "http://example.com/articles/1/relationships/comments",
"related": "http://example.com/articles/1/comments"
},
"data": [
{ "type": "comments", "id": "5" },
{ "type": "comments", "id": "12" }
]
}
},
"links": {
"self": "http://example.com/articles/1"
}
}
],
"included": [
{
"type": "people",
"id": "9",
"attributes": {
"firstName": "Dan",
"lastName": "Gebhardt",
"twitter": "dgeb"
},
"links": {
"self": "http://example.com/people/9"
}
},
{
"type": "comments",
"id": "5",
"attributes": {
"body": "First!"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "2" }
}
},
"links": {
"self": "http://example.com/comments/5"
}
},
{
"type": "comments",
"id": "12",
"attributes": {
"body": "I like XML better"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
}
},
"links": {
"self": "http://example.com/comments/12"
}
}
]
}
This returns the data in a way a team or group of developers can expect. When you use a spec it's a source of truth so there are no surprises along your software building journey.
What's super cool is that the active_model_serializers
gem comes with support for this format out of the box. You simply need to add a configuration setting to your Rails app and you're done.
To add it you can create a new initializer file (name it whatever you prefer) in config/initializers
# config/initializers/ams.rb
ActiveModelSerializers.config.adapter = :json_api
That then returns your data in the JSON API recommended spec. So cool!
Part 2 Notes
Routing in an API driven app is very important. Often times you want to version your API as well so if someone is using it and a bit behind on versions, their code won't simply break if you make changes.
With Rails, adding an API routing pattern is quite trivial. Here's the final pattern in use at the end of this part.
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :bands do
resources :members
end
end
namespace :v2 do
resources :members
resources :bands
end
end
end
Usually declaring a namespace wrapper around your entire API is wise. This ensures anything within is matched to the api
itself which makes for easier debugging as errors arise.
Versioning your API is a good convention to uphold as well. Most APIs in the wild follow this pattern as it makes updates for those who aren't ready for a new version you're releasing able to still use legacy versions.
Rails make namespacing and creating restful routing quite easy. You can even have basic routing for your normal rails views combined with a namespaced API in the same app following the monolith approach many Rails apps know and love.
Top comments (2)
Hey, nice tuto. It's impressive how simple is to build a REST API with Ruby on Rails. Love it.
I also think you hit a good point by mentioning a standard way to output JSON when scaling or the API is getting big. JSON output is one of those things that no one agrees how to return them 😅
Suggestion: do not use instance variable in API controllers. In Rails (IMO), they make more sense when working with views.
Nice comment... Instance variables make sense in controllers, so could be used in views:
Now we can see band's guitar on band page.
In API, this is done by Serializers.