API with Grape - IvanAlvarezEnri/dragon-squad-api GitHub Wiki

Basic usage and example of making an API with Grape

Project of the guide here

The point of this project is to learn how to use Grape, work and define its related tools, and as documentation for anyone who wants to understand how the core of the main project will work. This guide is also mentioning how to work with models and RSpec, and will serve as a reminder of good practices and steps to follow.

Good practices that I'll recommend following this project and any project (and as a reminder for myself):

  • Always write first the (failure) tests.
  • Try to write a code that explains itself, with a modular and single responsibility principle (SRP) based structure.
  • Working and implementing little and functional steps everytime.
  • After that, commit and push with a clear description with that what you done.

This readme.md could also be as example to how to document the main project readme with Markdown

What is Grape?

Grape is an API framework for ruby language, it is used to write RESTful API for the existing web application that is already written in Rails or Sinatra framework. It has inbuilt option like common convention, multiple data/response format, versioning etc.

Adventages of Grape in order to make an API with Rails

  • You can easily document the API with description without writing separate API document.
  • Easy and standard syntax of API endpoints.
  • Easy way to format data for typical response.
  • Adding new parameter and basic validation can be done easily.
  • Definition of routes and API body is very decent and developer friendly.
  • Grape API is much faster than any other API for rails application.
  • Versioning support.

Usage guide

Making the project

$ rails new grape_api_guide --api -T

You can indicate a database adding --database=postgresql , for more options: $ rails new --help

Gems that I will use

A good practice in terms of adding gems to your gemfile is to specify the version. You can find that here rubygems.org

Grape gems

Grape: Explained above.

Grape Entity: With Grape-Entity we could handle which attribute we would like to display in our API according to our models.

Grape On Rails Routes: Very useful gem to visualize API routes.

TDD gems

RSpec: For testing purposes we will use RSpec. The basic idea behind this concept is that of Test Driven Development where the tests are written first and the development is based on writing just enough code that will fulfill those tests followed by refactoring.

Faker: Easy and quickly way to populate fake data in DB.

Facotry Bot: Is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies.

Shoulda Matchers: Provides RSpec with additional matchers that if written by hand would be harder to write.

Database Cleaner: Cleans our test database to ensure a clean state in each test suite.


gem 'grape'
gem 'grape-entity'
gem 'grape_on_rails_routes'

[...]

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
  gem 'shoulda-matchers'
  gem 'database_cleaner'
end

$ bundle install

Configuring the project

Initialize the spec directory (where our tests will reside)

$ rails generate rspec:install

Configure the TDD environment on rails_helper.rb

# [...]
# Add additional requires below this line. Rails is not loaded until this point!
require 'database_cleaner'

# configure shoulda matchers to use rspec as the test framework and full matcher libraries for rails
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

# [...]
RSpec.configure do |config|
  # [...]
  # add `FactoryBot` methods
  config.include FactoryBot::Syntax::Methods

  # start by truncating all the tables but then use the faster transaction strategy the rest of the time.
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
    DatabaseCleaner.strategy = :transaction
  end

  # start the transaction strategy as examples are run
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end

end

Making the models

For this guide I'll work with 2 models, on a 1:M relationship. One Customer has many Orders and one Order belongs to a single Customer.

$ rails g model Customer name:string adress:string
$ rails g model Order name:string shipped:boolean delivered:boolean customer:references
$ rails db:migrate RAILS_ENV=test

Model specs

Following the TDD methodology, we should write the model specs first

This should work as a kick example on how to work with TDD

# on spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  it { should have_many(:orders).dependent(:destroy) }
  it { should validate_presence_of(:name) }
  it { should validate_presence_of(:adress) }
end

# on spec/models/order_spec.rb
require 'rails_helper'

RSpec.describe Order, type: :model do

  it { should belong_to(:customer) }

  it { should validate_presence_of(:name) }
  it { should validate_presence_of(:shipped) }
  it { should validate_presence_of(:delivered) }
end

Running rspec should fail

$ rspec

We need to validate the presence of that fields on models

# on models/customer.rb
class Customer < ApplicationRecord
  has_many :orders, dependent: :destroy
  validates_presence_of :name, :adress
end

# on models/order.rb
class Order < ApplicationRecord
  belongs_to :customer
  validates_presence_of :name
  validates_inclusion_of :shipped, :in => [true, false]
  validates_inclusion_of :delivered, :in => [true, false]
end

If you want to validate the presence of a boolean field (where the real values are true and false), you will want to use validates_inclusion_of :field_name, in: [true, false]. This is due to the way Object#blank? handles boolean values: false.blank? # => true.

Running rspec should pass the validations test

Test data through Factory Bot

We can make test and volatile data with Factory Bot

$ mkdir spec/factories && touch spec/factories/{customer.rb,order.rb}
# on factories/customer.rb
FactoryBot.define do
  factory :customer do
    name { Faker::TvShows::GameOfThrones.character }
    adress { Faker::TvShows::GameOfThrones.city }
  end
end

# on factories/order.rb
FactoryBot.define do
    factory :order do
        name { Faker::Commerce.product_name }
        shipped { Faker::Boolean.boolean }
        delivered { false }
        customer_id nil
    end
end

Then if we want, we can create instances of our models.

Test data through seeds.rb and DB

We can also populate our database filling seeds.rb to test our endpoints via postaman

require 'database_cleaner'
# This cleans after each rails db:seeds
DatabaseCleaner.clean_with(:truncation)

10.times do
    Customer.create!(
        name: Faker::TvShows::GameOfThrones.character,
        adress: Faker::TvShows::GameOfThrones.city
    )
end

customer_ids = Customer.ids

30.times do
    Order.create!(
        customer_id: customer_ids.sample,
        name: Faker::Commerce.product_name,
        shipped: Faker::Boolean.boolean,
        delivered: false
    )
end

And running then

$ rails db:seeds

You can play with the DB instances through the rails console $ rails c

Build the API with Grape

First step is to create our API folder within app and also a folder with the name of our API (I named it ebye)

$ mkdir -p app/api/ebye

We need to tell to our application where our API will be written, so we will add on application.rb

config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
config.autoload_paths += Dir[Rails.root.join('app', 'api/ebye/v1', '*')]

Our API needs a main file where we will declare the paths, so we need a base.rb file which will find inside ebye folder.

# on api/ebye/base.rb
module Ebye
    class Base < Grape::API
        mount Ebye::V1::Customers
        # This line above is the path to find our API, we will write all our API methods inside a Customers.rb
    end
end

Adding in our routes.rb a route to access to API from our app.

  mount Ebye::Base => '/'

This means that the API starting point is our base.rb

Making our first endpoint

Our first endpoint will be a simple GET /customers

Tests first

We need to create the folder request in order to write there our custom tests.

$ mkdir -p spec/request && touch spec/request/customer_spec.rb

We are also creating a support folder wich will contain helper files and methods to make our code modular and easier to read.

$ mkdir -p spec/support && touch spec/support/request_spec_helper.rb
module RequestSpecHelper
    def json
      JSON.parse(response.body)
    end
end

In order to be able to use this methods, we must add the following line to our rails_helper.rb

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

Then we are ready to make the first API test for customers. We can initialize test data thanks to our factory bot previous configuration.

# on customer_spec.rb
require 'rails_helper'

RSpec.describe 'customer API', type: :request do
    # Test data
    let!(:customers) { create_list(:customer, 15) }

    # GET /customers #######################
    describe 'GET /customers' do
        # make HTTP get request before each example
        before { get '/api/v1/customers' }

        it 'returns all customers' do
            expect(json).not_to be_empty
            expect(json.size).to eq(15)
        end

        it 'returns status code 200' do
            expect(response).to have_http_status(200)
        end
    end
end

The tests should fail! Thats because we didn't created the endpoint yet. Lets do it.

Making the API for customers

We will create the folder V1 which will contain our customers.rb file mentioned above. In this file we'll define some configurations for the api customers.

$ mkdir -p app/api/ebye/v1 && touch app/api/ebye/v1/customers.rb
# on app/api/ebye/v1/customers.rb
module Ebye
  module V1
    class Customers < Grape::API
      # With v1 we specify the version of our API
      version 'v1', using: :path
      # Tell our API that we allow only JSON
      format :json
      # We prefix the path of our API. Remind you, in route.rb we set route like that mount Ebye::Base => '/'
      # With this prefix we could access to our API instead '/api'
      prefix :api
      # Indicates customers routes
      resource :customers do
        # Description of our method and what we are expecting
        desc 'Return list of customers'
        get do
          # Method to return all customers
          customers = Customer.all
          present customers
        end
      end
    end
  end
end

Our first endpoint is finished. Using postman, you can test your API and should return all customers created before on seeds.rb .

You can also see the endpoint in the terminal by doing

$ rails grape:routes

Handle entities

Time to handle entities with grape. Entities allows to filter data from our models to choose which attributes we would like to display. Let’s clean the response we get on postman by creating entities folder in our API.

mkdir -p app/api/ebye/entities && touch app/api/ebye/entities/customer.rb

So here we will tell to our API that only exposes the information that we want to show.

module Ebye
    module Entities
        class Customer < Grape::Entity
            expose :name
            expose :adress
        end
    end
end

Now, to see if this is working, we should modify this line from the last endpoint we did on our API file customers.rb

present customers, with: Ebye::Entities::Customer

The output now should be something like this, without showing the id, created_at and updated_at

  [
    {
        "name": "Jason Lannister",
        "adress": "Oros"
    },
    {
        "name": "Willem Darry",
        "adress": "Pentos"
    }
  ]

API for get a single Customer

First of all we should test this feature adding this code on our customer_spec.rb


let(:customer_id) { customers.first.id }

# GET /customers/:id ####################################
describe 'GET /customers/:id' do
    before { get "/api/v1/customers/#{customer_id}" }

    context 'when the record exists' do

        it 'returns the customer' do
            expect(json).not_to be_empty
            expect(json['id']).to eq(customer_id)
        end

        it 'returns status code 200' do
            expect(response).to have_http_status(200)
        end
    end

    context 'when the record does not exist' do
        let(:customer_id) { 100 }

        it 'returns status code 404' do
            expect(response).to have_http_status(404)
        end

        it 'returns a not found message' do
            expect(response.body).to match("{\"message\":\"Couldn't find customer with 'id'=100\"}")
        end
    end
end

Now, in order to make this tests look green, we should write the endpoint on the customer API file

desc 'Return a specific customer'
# route_param :id Allows us to define namespace to pick up a customer thanks to its id
route_param :id do
    get do
        customer = Customer.find(params[:id])
        present customer, with: Ebye::Entities::Customer
    end
end

Now if we check our routes with rails grape:routes you will see the new one

GET  |  /api/:version/customers(.json)      |  v1  |  Return list of customers
GET  |  /api/:version/customers/:id(.json)  |  v1  |  Return a specific customer

You can test now this route on postman to see the magic works! http://localhost:3000/api/v1/customers/7

Adding orders to a customer

We need to nested order into customer resource beacuse each customer has many orders. We should add this code inside the resource :customers do [...] route_param :id do function in our customers.rb API

# We create our route to create a new order
resource :orders do
    desc 'Create a order.'
    # We set order params expected and allowed
    params do
        requires :order, type: Hash do
            requires :name, type: String, desc: 'Name of the Order.'
            requires :shipped, type: Boolean, desc: 'If shipped or not.'
            requires :delivered, type: Boolean, desc: 'If delivered or not.'
        end
    end
    # We use a post request to create a new order and after
    # it's just a classic way to create an instance inside nested resource
    post do
        @customer = Customer.find(params[:id])
        @order = Order.new(params[:order])
        @order = @customer.orders.create!(params[:order])
    end
end

Now we have create a new route which allows us to create new order for a specific customer.

rails grape:routes

GET  |  /api/:version/customers(.json)      |  v1  |  Return list of customers
GET  |  /api/:version/customers/:id(.json)  |  v1  |  Return a specific customer
POST  |  /api/:version/customers/:id/orders(.json)  |  v1  |  Create a order.

Order entity

If you tried to create a new order it should works so far but you can't display customers orders yet when you go to http://localhost:3000/api/v1/customers/1. To achieve this result we need to add a order entity

touch app/api/ebye/entities/order.rb
module Ebye
    module Entities
        class Order < Grape::Entity
            expose :name
            expose :shipped
            expose :delivered
        end
    end
end

We need to add a new line inside customer entity to display customer orders.

expose :orders, using: Ebye::Entities::Order

Now if you test http://localhost:3000/api/v1/customers/1 you should get something like

{
    "name": "Gerald Gower",
    "adress": "Qarth",
    "orders": [
        {
            "name": "Incredible Aluminum Car",
            "shipped": true,
            "delivered": true
        },
        {
            "name": "Heavy Duty Linen Gloves",
            "shipped": true,
            "delivered": true
        }
    ]
}

Entity for index Customer

Now if we search for all customers, we are displaying also all him orders, and maybe we don't want to do that. We can fix this creating an Index Entity.

touch app/api/ebye/entities/index.rb
module Ebye
    module Entities
        class Index < Grape::Entity
            expose :name
            expose :adress
        end
    end
end

So we need to change in our customers.rb with the correct path to reach the correct entity (index.rb)

present customers, with: Ebye::Entities::Index

Now we are getting what we want from each path, only name and adress on /customers and name, adress and orders on /customers/:id

Create an order via Postman

We created the POST route for orders nested on a specific customer but we didn't tried yet to make a petition on it. Let's try it with postman.

Postman

This should add a new order to our DB.

DB

:rocket:

References used to make this guide

Gems official docs (a lot)

My TDD API simple CRUD lecture exercice

My portfolio Rails christmas project

Xavi's TDD API with OAhut, JWT...

build-a-basic-api-with-grape

build-a-restful-json-api

how-to-build-a-rest-like-api-in-ruby

apidock validation methods

apidock.com