My Journey Building a Rails 5 API

Notes & thoughts on building my first API with Rails 5.

What am I making?

This app will be used by developers who write scripts that automate repetitive tasks in video games.

Developers will be able to create a profile for their scripts, and use the API to record the runtime and type/number of tasks automated.
ex: logs chopped, monsters killed, trees cut, fish caught, etc.

The data recorded could be used for:

Getting Started

Generate a Rails API only app using PostgreSQL since I’m hosting on Heroku.

$ rails new rsscriptstats --api --database=postgresql
An Interview with Heroku on Postgres

Setup RSpec

Setup RSpec before models to utilize its auto-generated tests.

group :development, :test do
  gem 'rspec-rails', '~> 3.6'
end
$ bundle install
$ rails generate rspec:install

Setup up Active Record

Models & Associations

Generate necessary models:

$ rails g devise_token_auth:install User auth
$ rails g scaffold Script name:string skill:string bot_for:string game_for:string
$ rails g scaffold Commit runtime:integer
$ rails g scaffold Stat task:string amount:float

Note: The User model was generated using devise_token_auth gem.

I learned:
You can set associations & delete scaffolds using the scaffold in command line.

rails d scaffold Stat task:string amount:float

Set model associations

The database is setup as such:
Versioning API Comic

Here are some of the database associations and test code:

#/app/models/script.rb
class Script < ApplicationRecord
  has_many :commits
end

#/app/models/commit.rb
class Commit < ApplicationRecord
  belongs_to :script
end

#/db/migrate/20171231022832_create_commits.rb
class CreateCommits < ActiveRecord::Migration[5.1]
  def change
    create_table :commits do |t|
      t.belongs_to :script, index: true #Add this line
      t.string :task
      t.float :amount
      t.timestamps
    end
  end
end

Seeding

test_script = Script.create(name: "YWoodcutter",
              skill: "Woodcutting",
              bot_for: "TRiBot",
              game_for: "Oldschool Runescape 07")


test_commit_1 = Commit.new(task: "Logs Cut",
                              amount: "5")
test_commit_1 = Commit.new(task: "Oak Cut",
                              amount: "10")

test_script.commits << test_commit_1
test_script.commits << test_commit_2

Versioning

Versioning API Comic

I came across multiple ways to handle versioning, research to see which fits your need best.
Different strategies explained here:
Versioning a Rails API by Chris

I decided on URL namespaceing since it was covered the most thoroughly online, and was mentioned in the Infinifum Rails Handbook.

Namespacing & Routing

Organizing my filestructure to better accommodate for versioned API.
Rails API Version Filestructure

You can namespace controllers by using the module keyword:
How to Namespace Controllers in Rails

module Api::V1
  class CommitsController < ApplicationController

    def index
      @commits = Commit.all

      render json: @commits
    end

    ...
  end
end

Define new route:

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :commits
    end
  end
end

Will also need to update the location parameter path in the controller to reflect the new route.
Explained in more detailed on this Stack Overflow answer

render json: @commit, status: :created, location: api_v1_commit_url(@commit)

http://localhost:3000/api/v1/commits/ will now hit your API!

Note:
I had to make sure to permit necessary parameters in the controller before successfully cURL POSTing to the endpoint.

Adding Authentication

I chose to use the trusty devise_token_auth gem.

Add devise_token_auth to gemfile

gem 'devise_token_auth'

Install dependencies

$ bundle

Generate a user model

$ rails g devise_token_auth:install User auth

If it’s an API only app, disable Devise flash messages in config/initializers/devise.rb

Devise.setup do |config|
    config.navigational_formats = [ :json ]
end

Migrate

$ rails db:migrate

Accepted Nested Attributes

#rsscriptstats/app/models/commit.rb
Accepted nested parameters in model.
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

class Commit < ApplicationRecord
  has_many :stats

  accepts_nested_attributes_for :stats
end

Allow the nested resources’ params in controller.

def commit_params
  params.require(:commit).permit(:task, :runtime, :script_id, :user_id, stats_attributes: [:task, :amount, :commit_id])
end

And here’s where I stopped writing…

To be continued…
Listed below are a ton of amazing Rails API guides & resources that greatly assisted during this process.