Implementing Pagination for API only Rails 5.1 App

Create a Rails 5.1 API server app:

rails new vueb --api

Add

gem 'faker'

to Gemfile and run bundle. We need the following fields in our model.

id : integer
plot : string
writer : string
upvotes : integer

Create a story resource using scaffold:

rails g scaffold story plot writer upvotes:integer

Refer the faker gem documentation, to find the methods to use.

plot - lorem - Faker::Lorem.sentence
writer - name -  Faker::Name.name    
upvotes - number - Faker::Number.between(1, 200) 

Create some records in seeds.rb:

100.times do
  Story.create(plot: Faker::Lorem.sentence, writer: Faker::Name.name, upvotes: Faker::Number.between(1, 200))
end

Migrate and seed the database.

rails db:migrate
rails db:seed

You can now view the JSON at the URL http://localhost:3000/stories

Pagination

Add the gem:

gem 'active_model_serializers'  

and run bundle.

Create config/initializers/active_model_serializers.rb:

ActiveModelSerializers.config.adapter = :json_api  

to use JSON API.

Add:

api_mime_types = %W(  
  application/vnd.api+json
  text/x-json
  application/json
)
Mime::Type.register 'application/vnd.api+json', :json, api_mime_types  

to the active_model_serializers initializer. You can see the options available to create serializer:

rails g serializer
Usage:
  rails generate serializer NAME [field:type field:type] [options]

Options:
  [--skip-namespace], [--no-skip-namespace]  # Skip namespace (affects only isolated applications)
  [--parent=PARENT]                          # The parent class for the generated serializer

Runtime options:
  -f, [--force]                    # Overwrite files that already exist
  -p, [--pretend], [--no-pretend]  # Run but do not make any changes
  -q, [--quiet], [--no-quiet]      # Suppress status output
  -s, [--skip], [--no-skip]        # Skip files that already exist

Description:
    Generates a serializer for the given resource.

Example:
    `rails generate serializer Account name created_at`

Create stories serializer:

rails g serializer stories

Expose the fields that needs to be serialized in the serializer.

class StoriesSerializer < ActiveModel::Serializer
  attributes :id, :plot, :writer, :upvotes

end

You will get the error:

No serializer found for resource: #<Story id: 3, plot: "This is the third record.", writer: "Daddy Care", upvotes: 56, created_at: "2017-06-02 01:23:06", updated_at: "2017-06-02 07:10:48">
[active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with Story::ActiveRecord_Relation (24.65ms)
Completed 200 O

Rename serializer to story_serializer.rb and rename the class to StorySerializer. The name of the model is story, so the serializer name must be StorySerializer.

class StorySerializer < ActiveModel::Serializer

Add the pagination gems to Gemfile and run bundle.

gem 'will_paginate'
gem 'api-pagination'

You can now hit the server using curl.

curl -I http://localhost:3000/stories
HTTP/1.1 200 OK
Link: <http://localhost:3000/stories?page=4>; rel="last", <http://localhost:3000/stories?page=2>; rel="next"
Per-Page: 30
Total: 103
Content-Type: application/json; charset=utf-8
ETag: W/"5ba579e6268a598b085567ea5c700470"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 2e730f86-d83e-493f-939d-6e4f80cd4cb5
X-Runtime: 0.025491
Vary: Origin

You can the link header that shows the links generated by the api-pagination gem according to the JSON API specification. If you hit: `http://localhost:3000/stories?page[number]=2&page[size]=30&page_number=2', you will see the links in the response:

links: {
self: "http://localhost:3000/stories?page%5Bnumber%5D=1&page%5Bsize%5D=30&page_number=2",
next: "http://localhost:3000/stories?page%5Bnumber%5D=2&page%5Bsize%5D=30&page_number=2",
last: "http://localhost:3000/stories?page%5Bnumber%5D=4&page%5Bsize%5D=30&page_number=2"
}

There is no previous link. Let's try the alternative to api-pagination, pager_api. Add the gem to Gemfile and run bundle.

gem 'pager_api'

Run the pager_api generator:

rails g pager_api:install

Change the controller:

paginate @stories, per_page: 10

You will get the error:

NoMethodError (undefined method `per' for #<Story::ActiveRecord_Relation:0x007fa50c415560>):

can't convert ActionController::Parameters into Integer rails

I found that the pager_api gem is better than api-pagination gem. The pager_api generates links like this:

links: {
self: "http://localhost:3000/stories?page%5Bnumber%5D=2&page%5Bsize%5D=10",
first: "http://localhost:3000/stories?page%5Bnumber%5D=1&page%5Bsize%5D=10",
prev: "http://localhost:3000/stories?page%5Bnumber%5D=1&page%5Bsize%5D=10",
next: "http://localhost:3000/stories?page%5Bnumber%5D=3&page%5Bsize%5D=10",
last: "http://localhost:3000/stories?page%5Bnumber%5D=11&page%5Bsize%5D=10"
}

To make the pagination work with the Majestiv Vue.js 2 book, I modified the controller:

  def index
    if params[:page] && params[:page][:number]
      stories = Story.paginate(page: params[:page][:number], per_page: 10)  
      total_pages = (Story.count / 10).ceil
      current_page = params[:page][:number]
    else
      stories = Story.all
      current_page = 1
    end

    pagination = {
    "current_page": current_page,
    "last_page": total_pages,
    "next_page_url": "http://localhost:3000/stories?page[number]=#{current_page+1}", 
    "prev_page_url": "http://localhost:3000/stories?page[number]=#{current_page-1}"
    }

    render json: {stories: stories, pagination: pagination}
  end

Note that the book does not use JSON API spec, so my controller code does not use any custom gems for pagination. The intent was to compare the options available and see which one would be a better gem when it is time to adopt the JSON API spec.

References


Related Articles


Create your own user feedback survey