DEV Community

Andy Leverenz
Andy Leverenz

Posted on • Originally published at web-crunch.com on

Create a Basic API with Ruby on Rails

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

To return real members you'll need to create some data.

rails console

Member.create(name: "Angus Young", band_id: 1)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# app/serializers/member_serializer.rb
class MemberSerializer < ActiveModel::Serializer
  attributes :id, :name
  belongs_to :band
end
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
cescquintero profile image
Francisco Quintero 🇨🇴 • Edited

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.

# Do this
def index
  bands = Band.all
end

# instead of 
def index
  @bands = Band.all
end
Enter fullscreen mode Exit fullscreen mode
Collapse
 
decentralizuj profile image
decentralizuj • Edited

Nice comment... Instance variables make sense in controllers, so could be used in views:

@band    = Band.find_by(id: params[:id])
@guitars = Guitar.where(band_id: @band.id).all
Enter fullscreen mode Exit fullscreen mode

Now we can see band's guitar on band page.

In API, this is done by Serializers.