Step 1: Basic Resource

View the Diff

We’ll be working with a single database table, employees:

id first_name last_name age created_at updated_at
1 Homer Simpson 39 2018-09-04 2018-09-04
2 Waylon Smithers 65 2018-09-04 2018-09-04
3 Monty Burns 123 2018-09-04 2018-09-04

The Rails Stuff 🚂

Use the built-in generator to create the database table and corresponding ActiveRecord model:

$ bin/rails g model Employee first_name:string last_name:string age:integer
$ bin/rails db:migrate

Now let’s seed some random development data, using Faker (which was installed in Step 0):

# db/seeds.rb
Employee.delete_all # Ensure the DB is cleaned each run

100.times do
  Employee.create! first_name: Faker::Name.first_name,
    last_name: Faker::Name.last_name,
    age: rand(20..80)
end

Run this seed file with

$ bin/rails db:seed

The Graphiti Stuff 🎨

Just like Rails, Graphiti has built-in generators. Let’s generate the corresponding Resource for our Employee model:

$ bin/rails g graphiti:resource Employee first_name:string last_name:string age:integer created_at:datetime updated_at:datetime

This generated a few things, but for now let’s focus on EmployeeResource:

class EmployeeResource < ApplicationResource
  attribute :first_name, :string
  attribute :last_name, :string
  attribute :age, :integer
  attribute :created_at, :datetime, writable: false
  attribute :updated_at, :datetime, writable: false
end

This code defined the RESTful Resource we want our API to expose. Let’s run our server and see what it does:

$ bin/rails s

Visit localhost:3000/api/v1/employees. You should see a JSONAPI Response:


jsonapi


If you find the payload a little intimidating, add .json to the URL for a more traditional response:


json


There’s .xml, too:


xml


These are all different renderings of the same EmployeeResource.

Resources are comprised of Attributes:

# app/resources/employee_resource.rb
attribute :first_name, :string

Each attribute defines behavior for:

  • Reading (display)
  • Writing
  • Sorting
  • Filtering
  • Fieldsets

Let’s start with simple display, turning first_name into all capital letters:

# app/resources/employee_resource.rb
attribute :first_name, :string do
  # @object is your model instance
  @object.first_name.upcase
end

Which gives us:


This is the most important thing to understand about Resources: they are just a collection of defaults, all of which can be overridden. In other words:

attribute :first_name

# is the same as

attribute :first_name do
  @object.first_name
end

We’ll go into further Resource customizations over the course of this tutorial. For now, let’s just verify our out-of-the-box defaults:

  • Sort by first_name ascending: http://localhost:3000/api/v1/employees?sort=first_name
  • Sort by first_name descending: http://localhost:3000/api/v1/employees?sort=-first_name
  • Return only age and created_at in the response: http://localhost:3000/api/v1/employees?fields[employees]=age,created_at
  • Filter on first_name:
    • Case-insensitive: http://localhost:3000/api/v1/employees?filter[first_name]=bob
    • Case-sensitive: http://locahost:3000/api/v1/employees?filter[first_name][eql]=Bob
    • Prefix: http://localhost:3000/api/v1/employees?filter[first_name][prefix]=b
    • Suffix: http://localhost:3000/api/v1/employees?filter[first_name][suffix]=ob
    • Contains: http://localhost:3000/api/v1/employees?filter[first_name][match]=o
  • Filter on age:
    • Equal: http://localhost:3000/api/v1/employees?filter[age]=39
    • Greater Than: http://localhost:3000/api/v1/employees?filter[age][gt]=39
    • Greater Than or Equal To: http://localhost:3000/api/v1/employees?filter[age][gte]=39
    • Less Than: http://localhost:3000/api/v1/employees?filter[age][lt]=65
    • Less Than or Equal To: http://localhost:3000/api/v1/employees?filter[age][lte]=65
  • Paginate
    • 10 per page: http://localhost:3000/api/v1/employees?page[size]=10
    • 5 per page, third page: http://localhost:3000/api/v1/employees?page[number]=3

Write operations are easiest to verify with integration tests, which were created when we generated our Resource. Let’s take a look at the test for creating Employees:

# spec/api/v1/employees/create_spec.rb

RSpec.describe "employees#create", type: :request do
  subject(:make_request) do
    jsonapi_post "/api/v1/employees", payload
  end

  describe 'basic create' do
    let(:payload) do
      {
        data: {
          type: 'employees',
          attributes: {
            # ... your attrs here
          }
        }
      }
    end

    it 'works' do
      expect(EmployeeResource).to receive(:build).and_call_original
      expect {
        make_request
      }.to change { Employee.count }.by(1)
      expect(response.status).to eq(201)
    end
  end
end

This is an API Spec, which tests high-level end-to-end functionality. We know that if our API receives a POST with the given payload, an Employee will be created and a 201 response code will be returned.

API specs are high-level - often they won’t be changed past this initial boilerplate. For testing logic, use a Resource Spec. These integration tests hit the database and run logic, but operate without a specific request or response:

# spec/api/v1/employees/create_spec.rb
RSpec.describe EmployeeResource, type: :resource do
  describe 'creating' do
    let(:payload) do
      {
        data: {
          type: 'employees',
          attributes: {
            first_name: 'Jane'
            last_name: 'Doe'
            age: 30
          }
        }
      }
    end

    let(:instance) do
      EmployeeResource.build(payload)
    end

    it 'works' do
      expect {
        expect(instance.save).to eq(true)
      }.to change { Employee.count }.by(1)
      employee = Employee.last
      expect(employee.first_name).to eq('Jane')
      expect(employee.last_name).to eq('Doe')
      expect(employee.age).to eq(30)
    end
  end
end

In other words: API specs test Endpoints (request, response, middleware, etc), Resource specs test only the Resource (actual application logic). Read more in our Testing Guide.

Before we run these specs, we need to edit our factories to ensure dynamic, randomized data. Let’s change this:

# spec/factories/employee.rb

FactoryBot.define do
  factory :employee do
    first_name { "MyString" }
    last_name { "MyString" }
    age { 1 }
  end
end

To

# spec/factories/employee.rb

FactoryBot.define do
  factory :employee do
    first_name { Faker::Name.first_name }
    last_name { Faker::Name.last_name }
    age { rand(20.80) }
  end
end

Now undo the capitalization change to attribute :first_name, and run the generated specs:

$ bin/rspec

You’ll see 11 tests pass, with 3 pending. One of the pending specs was autogenerated by rails - you can delete spec/models/employee_spec.rb for now.

That leaves us with two “update” specs. These are marked pending so you can manage the data yourself. Follow the comments in these specs to add attributes and get them passing.