Api On Rails PDF
Api On Rails PDF
APIs on Rails
Building REST APIs with Rails
Abraham Kuri
ii
Contents
1 Introduction 1
1.1 Conventions on this book . . . . . . . . . . . . . . . . . . . . 3
1.2 Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.1 Development environments . . . . . . . . . . . . . . 4
1.3 Initializing the project . . . . . . . . . . . . . . . . . . . . . . 9
1.3.1 Installing Pow or Prax . . . . . . . . . . . . . . . . . 9
1.3.2 Gemfile and Bundler . . . . . . . . . . . . . . . . . . 11
1.4 Version Control . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2 The API 17
2.1 Planning the application . . . . . . . . . . . . . . . . . . . . 17
2.2 Setting the API . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2.1 Routes, Constraints and Namespaces . . . . . . . . . 20
2.2.2 Api versioning . . . . . . . . . . . . . . . . . . . . . 24
2.2.3 Improving the versioning . . . . . . . . . . . . . . . . 25
2.3 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
iii
iv CONTENTS
4 Refactoring tests 57
4.1 Refactoring the json response . . . . . . . . . . . . . . . . . . 60
4.2 Refactoring the format param . . . . . . . . . . . . . . . . . . 62
4.3 Refactor before actions . . . . . . . . . . . . . . . . . . . . . 63
4.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
5 Authenticating users 67
5.1 Stateless and sign in failure . . . . . . . . . . . . . . . . . . . 68
5.1.1 Authentication token . . . . . . . . . . . . . . . . . . 68
5.1.2 Sessions controller . . . . . . . . . . . . . . . . . . . 71
5.2 Current User . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.3 Authenticate with token . . . . . . . . . . . . . . . . . . . . . 79
5.4 Authorize actions . . . . . . . . . . . . . . . . . . . . . . . . 81
5.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
6 User products 87
6.1 Product model . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.1.1 Product bare bones . . . . . . . . . . . . . . . . . . . 88
6.1.2 Product validations . . . . . . . . . . . . . . . . . . . 90
6.1.3 Product/User association . . . . . . . . . . . . . . . . 91
6.2 Products endpoints . . . . . . . . . . . . . . . . . . . . . . . 95
6.2.1 Show action for products . . . . . . . . . . . . . . . . 96
6.2.2 Products list . . . . . . . . . . . . . . . . . . . . . . . 98
6.2.3 Exploring with Sabisu . . . . . . . . . . . . . . . . . 99
6.2.4 Creating products . . . . . . . . . . . . . . . . . . . . 100
CONTENTS v
10 Optimization 179
10.1 Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
10.1.1 Products . . . . . . . . . . . . . . . . . . . . . . . . 180
10.1.2 Orders . . . . . . . . . . . . . . . . . . . . . . . . . . 185
10.1.3 Refactoring pagination . . . . . . . . . . . . . . . . . 186
10.2 Background Jobs . . . . . . . . . . . . . . . . . . . . . . . . 189
10.3 API Caching . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
10.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
About the author
Abraham Kuri is a Rails developer with 5 years experience. His experience
includes working as a freelancer building software products and more recently
to collaborate into the open source community. He developed Furatto a front
end framework built with Sass, Sabisu the next generation api explorer for your
Rails app and have collaborated on other projects. He is a Computer Science
graduate of ITESM and founded two companies in Mexico (Icalia Labs and
Codeando Mexico).
vii
viii ABOUT THE AUTHOR
Copyright and license
API’s on Rails: Building REST API’s with Rails. Copyright l’ 2014 by Abra-
ham Kuri. All source code in the tutorial is available jointly under the MIT
License and the Beerware License.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* Abraham Kuri wrote this code. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return.
* ----------------------------------------------------------------------------
*/
ix
x COPYRIGHT AND LICENSE
Chapter 1
Introduction
Welcome to APIs on Rails a tutorial on steroids on how to buid your next
API with Rails. The goal of this book is to provide an answer on how to de-
velop a RESTful API following the best practices out there, along with my
own experience. By the time you are done with API’s on Rails you should
be able to build your own API and integrate it with any clients such as a
web browser or your next mobile app. The code generated is built on top of
Rails 4 which is the current version, for more information about this check out
http://rubyonrails.org/. The most up-to-date version of the API’s on Rails can
be found on http://apionrails.icalialabs.com; don’t forget to update your offline
version if that is the case.
The intention with this book it’s not teach just how to build an API with
Rails rather to teach you how to build scalable and maintanable API with Rails,
which means taking your current Rails knowledge to the next level when on this
approach. In this journey we are going to take, you will learn to:
1
2 CHAPTER 1. INTRODUCTION
I highly recommend you go step by step on this book, try not to skip chap-
ters, as I mention tips and interesting facts for improving your skills on each on
them. You can think yourself as the main character of a video game and with
each chapter you’ll get a higher level.
In this first chapter I will walk you through on how to setup your environ-
ment in case you don’t have it already. We’ll then create the application called
market_place_api. I’ll emphasize all my effort into teaching you all the
best practices I’ve learned along the years, so this means right after initializ-
ing(Section 1.3) the project we will start tracking it with Git (Section 1.4).
In the next chapters we will be building the application to demonstrate a
simple workflow I use on my daily basis. We’ll develop the whole application
using test driven development (TDD), getting started by explaining why you
want to build an API’s for your next project and decising wheter to use JSON
or XML as the response format. From Chapter 3 to Chapter 8 we’ll get our
hands dirty and complete the foundation for the application by building all
the necessary endpoints, securing the API access and handling authentication
through headers exchange. Finally on the last chapter (Chapter 11) we’ll add
some optimization techniques for improving the server responses.
The final application will scratch the surface of being a market place where
users will be able to place orders, upload products and more. There are plenty
of options out there to set up an online store, such as Shopify, Spree or Ma-
gento.
By the end or during the process(it really depends on your expertise), you
will get better and be able to better understand some of the bests Rails resources
out there. I also took some of the practices from these guys and brought them
to you:
• Railscasts
• CodeSchool
• Json api
1.1. CONVENTIONS ON THIS BOOK 3
I’ll be using some guidelines related to the language, what I mean by this
is:
• “Prefer” indicates that from the 2 options, the first it’s a better fit
If for any reason you encounter some errors when running a command,
rather than trying to explain every possible outcome, I’ll will recommend you
to ‘google it’, which I don’t consider a bad practice or whatsoever. But if you
feel like want to grab a beer or have troubles with the tutorial you can always
shout me tweet or email me. I’m always willing to know you guys!
• Bundler gem
for Rails development an IDE is way to much, but some other might find that
the best way to go, so if that it’s your case I recommend you go with RadRails
or RubyMine, both are well supported and comes with many integrations out
of the box.
Now for those who are more like me, I can tell you that there are a lot of
options out there which you can customize via plugins and more.
• Text editor: I personally use vim as my default editor with janus which
will add and handle many of the plugins you are probably going to use. In
case you are not a vim fan like me, there are a lot of other solutions such
as Sublime Text which is a cross-platform easy to learn and customize
(this is probably your best option), it is highly inspired by TextMate (only
available for Mac OS). A third option is to use a more recent text editor
from the guys at Github called Atom, it’s a promising text editor made
with Javascript, it is easy to extend and customize to meet your needs,
give it a try. Any of the editors I present will do the job, so I’ll let you
decide which one fits your eye.
• Terminal: If you decided to go with kaishi for setting the environment
you will notice that it sets the default shell to zsh, which I highly rec-
ommend. For the terminal, I’m not a fan of the Terminal app that comes
out of the box if you are on Mac OS, so check out iTerm2, which is a ter-
minal replacement for Mac OS. If you are on Linux you probable have a
nice terminal already, but the default should work just fine.
Browsers
When it comes to browsers I would say Chrome immediately, but some
other developers may say Firefox or even Safari. Any of those will help you
build the application you want, they come with nice inspector not just for the
dom but for network analysis and many other features you might know already.
A note on tools
All right, I understand that you may not want to include every single pack-
age that comes with kaishi, and that is fair, or maybe you already have some
tools installed, well I’ll describe you how to install the bare bones you need to
get started:
6 CHAPTER 1. INTRODUCTION
Package manager
• Mac OS: There are many options to manage how you install packages on
your Mac, such as Mac Ports or Homebrew, both are good options but I
would choose the last one, I’ve encountered less troubles when installing
software and managing it. To install brew just run the command below:
• Linux: You are all set!, it really does not matter if you are using apt,
pacman, yum as long you feel comfortable with it and know how to in-
stall packages so you can keep moving forward.
Git
We will be using Git a lot, and you should use it too not just for the purpose
of this tutorial but for every single project.
• Mac OS:
• Linux:
Ruby
There are many ways in which you can install and manage ruby, and by
now you should probably have some version installed (1.8) if you are on Mac
OS, to see which version you have, just type:
$ ruby -v
1.2. GETTING STARTED 7
Rails 4 requires you to install version 1.9 or higher, and in order to accom-
plish this I recommend you to start using Ruby Version Manager (RVM) or
rbenv, any of these will allow you to install multiple versions of ruby. I re-
cently changed from RVM to rbenv and it’s great, so any of these two options
you choose is fine. On this tutorial we’ll be using rbenv.
A note for Mac OS: if you are using Mac just keep in mind you have to have
installed the Command Line Tools for Xcode.
Mac OS:
To get started with the ruby installation, type in:
Next you have to set up the just installed version of ruby as the default one:
The rehash command is supposed to run everytime you install a new ruby
version or a gem. Seems like a lot? check out rbenv-gem-rehash brew formula
to mitigate this.
For more information about customization or other types of installation
checkout out the project documentation.
Linux:
The first steo is to setup some dependencies for Ruby:
$ cd
$ git clone git://github.com/sstephenson/rbenv.git .rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.profile
$ echo 'eval "$(rbenv init -)"' >> ~/.profile
$ exec $SHELL
On some cases if you are on a Mac OS, you will need to install some extra
libraries:
We then install the necessary gems and ignore documentation for each gem:
$ rails -v
Rails 4.0.5
1.3. INITIALIZING THE PROJECT 9
Databases
I highly recommend you install Postgresql to manage your databases, but
for simplicity we’ll be using SQlite. If you are using Mac OS you should be
ready to go, in case you are on Linux, don’t worry we have you covered:
or
As you may guess, the commands above(Listing 1.1) will generate the bare
bones of your Rails application. The next step is to add some gems we’ll be
using to build the api.
Installing Pow:
Pow only works on Mac OS, but don’t worry there is an alternative which
mimics the functionality on Linux. To install it just type in:
$ curl get.pow.cx | sh
And that’s it you are all set. You just have to symlink the application in
order to set up the Rack app.
First you go the ~/.pow directory:
$ cd ~/.pow
$ ln -s ~/workspace/market_place_api
Remember to change the user directory to the one matches yours. You can
now access the application through http://market_place_api.dev/. Your appli-
cation should be up a running by now like the one shown on Figure 1.1.
Installing Prax
For linux users only, I extracted the instructions from the official documen-
tation, so for any further documentation you should refer to the README file
on the github repository.
It is recommended that you clone the repository under the /opt directory
and then run the installer which will set the port forwarding script and NSS-
witch extension.
$ cd /opt/prax/
$ ./bin/prax install
$ cd ~/workspace/market_place_api
$ prax link
If you want to start the prax server automatically, add this line to the .profile
file:
prax start
When using prax, you have to specify the port for the URL, in this case
http://market_place_api.dev:3000
You should see the application up and running, see Figure 1.1.
#Api gems
gem 'active_model_serializers'
group :doc do
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', require: false
end
Notice that I remove the jbuilder and turbolinks gems, as we are not
really going to use them anyway.
It is a good practice also to include the ruby version used on the whole
project, this prevents dependencies to break if the code is shared among differ-
ent developers, whether if is a private or public project.
It is also important that you update the Gemfile to group the different gems
into the correct environment (Listing 1.3):
gem 'sqlite3'
end
...
This as you may recall will prevent sqlite from being installed or required
when you deploy your application to a server provider like Heroku.
Note about deployment: Due to the structure of the application we are not
going to deploy the app to any server, but we will be using Pow by Basecamp.
If you are using Linux there is a similar solution called Prax by ysbaddaden.
See Section 1.3.1
Once you have this configuration set up, it is time to run the bundle
install command to integrate the corresponding dependencies:
$ bundle install
Fetching source index for https://rubygems.org/
.
.
.
After the command finish its execution, it is time to start tracking the project
with git (Section 1.4)
Replace the last command editor("mvim -f") with the one you installed
"subl -w" for SublimeText ,"mate -w" for TextMate, or "gvim -f" for
gVim.
So it is now time to init the project with git. Remember to navigate to the
root directory of the market_place_api application:
$ git init
Initialized empty Git repository in ~/workspace/market_place_api/.git/
The next step is to ignore some files that we don’t want to track, so your
.gitignore file should look like the one shown below (Listing 1.4):
After modifiying the .gitignore file we just need to add the files and
commit the changes, the commands necessary are shown below:
1.5. CONCLUSION 15
$ git add .
$ git commit -m "Initializes the project"
then:
As we move forward with the tutorial, I’ll be using the practices I follow
on my daily basis, this includes working with branches, rebasing, squash
and some more. For now you don’t have to worry if some of these don’t sound
familiar to you, I walk you through them in time.
1.5 Conclusion
It’s been a long way through this chapter, if you reach here let me congratulate
you and be sure that from this point things will get better. If you feel like want
16 CHAPTER 1. INTRODUCTION
to share how are you doing with the tutorial, I’ll be happy to read it, a nice
example is shown below:
I just finished the first chapter of Api on Rails tutorial by @kurenn!
So let’s get our hands dirty and start typing some code!
Chapter 2
The API
In this section I’ll outline the application, by now you should have the bare
bones of the application, as shown in Section 1.3, if you did not read it I rec-
ommend you to do it.
You can clone the project until this point with:
And as a quick recap, we really just update the Gemfile to add the active_model_ser
gem, see Listing 1.2 for more information.
17
18 CHAPTER 2. THE API
Figure 2.1
2.2. SETTING THE API 19
And in REST you may call a URL with an especific HTTP request, in this
case with a GET request (Listing 2.2)
• Follow the standard HTTP Methods such as GET, POST, PUT, DELETE.
Resource GET
http://example.com/resources Reads the resource or resources defined by the UR
This might not be clear enough or may look like a lot of information to
digest but as we move on with the tutorial, hopefully it’ll get a lot easier to
understand.
First of all erase all commented code that comes within the file, we are not
gonna need it. Then commit it, just as a warm up:
We then add that namespace into our routes.rb file (Listing 2.5):
22 CHAPTER 2. THE API
$ rails c
Loading development environment (Rails 4.0.2)
2.1.0 :001 > Mime::SET.collect(&:to_s)
=> ["text/html", "text/plain", "text/javascript", "text/css", "text/calendar",
"text/csv", "image/png", "image/jpeg", "image/gif", "image/bmp",
"image/tiff", "video/mpeg", "application/xml", "application/rss+xml",
"application/atom+xml", "application/x-yaml", "multipart/form-data",
"application/x-www-form-urlencoded", "application/json", "application/pdf",
"application/zip"]
This is important because we are going to be working with JSON, one of the
built-in MIME types accepted by Rails, so we just need to specify this format
as the default one (Listing 2.6):
Up to this point we have not made anything crazy, what we want to achieve
next is how to generate a base_uri under a subdomain, in our case something
2.2. SETTING THE API 23
Listing 2.7: Routes with a namespace, default format and subdomain defined
MarketPlaceApi::Application.routes.draw do
# Api definition
namespace :api, defaults: { format: :json },
constraints: { subdomain: 'api' }, path: '/' do
# We are going to list our resources here
end
end
You can find many approaches to set up the base_uri when building an api
following different patterns, assuming we are versioning our api (Section 2.2.2):
URI Pattern Description
api.example.com/ I my opinion this is the way to go, gives you a better interface and
example.com/api/ This pattern is very common, and it is actually a good way to go
example.com/api/v1 This seems like a good idea, by setting the version of the api thro
Don’t worry about versioning right now, I’ll walk through it on (section 2.2.2)
Time to commit:
24 CHAPTER 2. THE API
All right take a deep breath, drink some water, and let’s get going.
MarketPlaceApi::Application.routes.draw do
# Api definition
namespace :api, defaults: { format: :json },
constraints: { subdomain: 'api' }, path: '/' do
# We are going to list our resources here
end
end
Now it is time to set up some other constraints for versioning purposes. You
should care about versioning your application from the beginning since this will
give a better structure to your api, and when changes need to be done, you can
give developers who are consuming your api the opportunity to adapt for the
new features while the old ones are being deprecated. There is an excellent
railscast explaining this.
In order to set the version for the api, we first need to add another directory
under the api we created on (Section 2.2.1):
$ mkdir app/controllers/api/v1
2.2. SETTING THE API 25
This way we can scope our api into different versions very easily, now we
just need to add the necessary code to the routes.rb file (Listing 2.8)
By this point the API is now scoped via de URL. For example with the
current configuration an end point for retrieving a product would be like:
http://api.marketplace.dev/v1/products/1
HTTP header fields are components of the message header of requests and
responses in the Hypertext Transfer Protocol (HTTP). They define the operating
parameters of an HTTP transaction.
A common list of used headers is presented below:
Header Name Description
Accept Content-Types that are acceptable for the response
Authorization Authentication credentials for HTTP authentication
Content-Type The MIME type of the body of the request (used with POST and PUT re
Origin Initiates a request for cross-origin resource sharing (asks server for an ‘A
User-Agent The user agent string of the user agent
In Rails is very easy to add this type versioning through an Accept header.
We will create a class under the lib directory of your rails app, and remember
we are doing TDD so first things first. (Listing 2.11).
First we need to add our testing suite, which in our case is going to be Rspec
(Listing 2.9):
$ bundle install
Finally we install the rspec and add some configuration to prevent views
and helpers tests from being generated:
$ rails g rspec:install
config.autoload_paths += %W(\#{config.root}/lib)
If everything went well it is now time to add a spec directory under lib
and add the api_constraints_spec.rb:
$ mkdir lib/spec
$ touch lib/spec/api_constraints_spec.rb
describe ApiConstraints do
let(:api_constraints_v1) { ApiConstraints.new(version: 1) }
28 CHAPTER 2. THE API
describe "matches?" do
Let me walk you through the code. We are initialising the class with an
options hash, which will contain the version of the api, and a default value
for handling the default version. We provide a matches? method which the
router will trigger for the constraint to see if the default version is required or
the Accept header matches the given string.
The implementation looks likes this(Listing 2.12)
def matches?(req)
@default || req.headers['Accept'].include?("application/vnd.marketplace.v#{@version}")
end
end
As you imagine we need to add the class to our routes.rb file and set it
as a constraint scope option.(Listing 2.13):
2.3. CONCLUSION 29
MarketPlaceApi::Application.routes.draw do
# Api definition
namespace :api, defaults: { format: :json },
constraints: { subdomain: 'api' }, path: '/' do
scope module: :v1,
constraints: ApiConstraints.new(version: 1, default: true) do
# We are going to list our resources here
end
end
end
The configuration above now handles versioning through headers, and for
now the version 1 is the default one, so every request will be redirected to that
version, no matter if the header with the version is present or not.
Before we say goodbye, let’s run our first tests and make sure everything is
nice and green:
2.3 Conclusion
It’s been a long way, I know, but you made it, don’t give up this is just our small
scaffolding for something big, so keep it up. In the meantime and I you feel
curious there are some gems that handle this kind of configuration:
• RocketPants
• Versionist
30 CHAPTER 2. THE API
I’m not covering those in here, since we are trying to learn how to actually
implement this kind of functionality, but it is good to know though. By the way
the code up to this point is here
Wanna tweet about your accomplishment?:
I just finished the second chapter of Api on Rails tutorial by @kurenn!
Chapter 3
As you can already imagine there are a lot of authentication solutions for
Rails, AuthLogic, Clearance and Devise. We will be using the last one (Box 3.1),
which offers a great way to integrate not just basic authentication, but many
other modules for further use.
31
32 CHAPTER 3. PRESENTING THE USERS
• Database Authenticable
• Omniauthable
• Confirmable
• Recoverable
• Registerable
• Rememberable
• Trackable
• Timeoutable
• Validatable
• Lockable
If you have not work with devise before, I recommend you visit the reposity
page and read the documentation, there are a lot of good examples out there too.
Just make sure you are on the master branch before checking out.
3.1. USER MODEL 33
#Api gems
gem 'active_model_serializers'
group :doc do
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', require: false
end
group :development do
gem 'sqlite3'
end
group :test do
gem "rspec-rails"
gem "factory_girl_rails"
gem 'ffaker'
end
gem "devise"
Then run the bundle install command to install it. Once the bundle
command finishes, we need to run the devise install generator:
34 CHAPTER 3. PRESENTING THE USERS
$ rails g devise:install
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
.
.
.
By now and if everything went well we will be able to generate the user
model through the devise generator:
From now every time we create a model, the generator will also create a
factory file for that model(Listing 3.2). This will help us to easily create test
users and facilitate our tests writing.
$ rake db:migrate
== DeviseCreateUsers: migrating ==============================================
-- create_table(:users)
-> 0.0031s
-- add_index(:users, :email, {:unique=>true})
3.1. USER MODEL 35
-> 0.0010s
-- add_index(:users, :reset_password_token, {:unique=>true})
-> 0.0004s
== DeviseCreateUsers: migrated (0.0047s) =====================================
$ rake db:test:prepare
Let’s commit this, just to keep our history points very atomic.
$ git add .
$ git commit -m "Adds devise user model"
[chapter3 be4e03f] Adds devise user model
10 files changed, 423 insertions(+), 1 deletion(-)
create mode 100644 app/models/user.rb
create mode 100644 config/initializers/devise.rb
create mode 100644 config/locales/devise.en.yml
create mode 100644 db/migrate/20140622003340_devise_create_users.rb
create mode 100644 db/schema.rb
create mode 100644 spec/factories/users.rb
create mode 100644 spec/models/user_spec.rb
Once we’d added the attributes it is time to test our User model (List-
ing 3.4).
describe User do
before { @user = FactoryGirl.build(:user) }
subject { @user }
it { should respond_to(:email) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
end
....
$ git add .
$ git commit -am 'Adds user firsts specs'
[chapter3 80b4cb6] Adds user firsts specs
2 files changed, 12 insertions(+), 1 deletion(-)
3.1. USER MODEL 37
But this can be improved with a gem called shoulda-matchers, let’s add
it to our Gemfile:
.
.
.
group :test do
gem "rspec-rails"
gem "factory_girl_rails"
gem 'ffaker'
gem "shoulda-matchers"
end
.
.
.
As usual before adding a gem, run the bundle command to install it.
Shoulda-matchers help us refactor our tests(Listing 3.5) like so:
it { should validate_presence_of(:email) }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
it { should validate_confirmation_of(:password) }
it { should allow_value('[email protected]').for(:email) }
$ git add .
$ git commit -m "Adds shoulda matchers for spec refactors"
[chapter3 9f8c6c1] Adds shoulda matchers for spec refactors
3 files changed, 10 insertions(+)
For a full list of HTTP method check out the article on Wikipedia talking about
it
To keep our code nicely organised, we will create some directories under
the controller specs directory in order to be consistent with our current setup
(Listing 2.4). There is also another set up out there which uses instead of the
controllers directory a request or integration directory, I this case I
like to be consistent with the app/controllers directory.
$ mkdir -p spec/controllers/api/v1
$ mv spec/controllers/users_controller_spec.rb spec/controllers/api/v1
require 'spec_helper'
describe Api::V1::UsersController do
end
describe Api::V1::UsersController do
before(:each) { request.headers['Accept'] = "application/vnd.marketplace.v1" }
So far, the tests look good, we just need to add the implementation(Listing 3.7),
it is extremely simple:
def show
respond_with User.find(params[:id])
end
end
If you run the tests now with bundle exec rspec spec/controllers
you will see an error message similar to this:
FF
Failures:
3.2. BUILDING USERS ENDPOINTS 41
require 'api_constraints'
MarketPlaceApi::Application.routes.draw do
devise_for :users
# Api definition
namespace :api, defaults: { format: :json },
constraints: { subdomain: 'api' }, path: '/' do
scope module: :v1,
constraints: ApiConstraints.new(version: 1, default: true) do
# We are going to list our resources here
resources :users, :only => [:show]
end
end
end
If you try again bundle exec rspec spec/controllers you will see
your all your tests passing.
As usual and after adding some bunch of code we are satisfied with, we
commit the changes:
$ git add .
$ git commit -m "Adds show action the users controller"
42 CHAPTER 3. PRESENTING THE USERS
This will throw us an error, well you might expect that already, we don’t
have a user with id 1, let’s create it first through the terminal:
$ rails console
Loading development environment (Rails 4.0.2)
2.1.0 :001 > User.create({email: "[email protected]",
password: "12345678",
password_confirmation: "12345678"})
So there you go, you now have a user record api endpoint. If you are having
problems with the response and double checked everything is well assembled,
well then you might need to visit the application_controller.rb file and
update it a little bit like so (Listing 3.8):
$ git add .
$ git commit -m "Updates application controller to prevent CSRF exception from being raised"
it "renders the json representation for the user record just created" do
user_response = JSON.parse(response.body, symbolize_names: true)
44 CHAPTER 3. PRESENTING THE USERS
it "renders the json errors on why the user could not be created" do
user_response = JSON.parse(response.body, symbolize_names: true)
expect(user_response[:errors][:email]).to include "can't be blank"
end
There is a lot of code up there(Listing 3.9) but don’t worry I’ll walk you
through it:
• We need to validate to states on which the record can be, valid or invalid.
In this case we are using the context clause to achieve this scenarios.
• In case everything goes smooth, we should return a 201 HTTP code
which means a record just got created, as well as the json representa-
tion of that object.
• In case of any errors, we have to return a 422 HTTP code which stands
for Unprocessable Entity meaning the server could save the record.
We also return a json representation of why the resource could not be
saved.
Time to implement some code and make our tests pass (Listing 3.10)
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
.
.
.
Remember that each time we add an enpoint we have to add that action into
our routes.rb file
As you can see the implementation is fairly simple, we also added the
user_params private method to sanitize the attribute to be assigned through
mass-assignment. Now if we run our tests, they all should be nice and green:
46 CHAPTER 3. PRESENTING THE USERS
$ git add .
$ git commit -m "Adds the user create endpoint"
[chapter3 9222e75] Adds the user create endpoint
3 files changed, 52 insertions(+), 1 deletion(-)
it "renders the json errors on whye the user could not be created" do
user_response = JSON.parse(response.body, symbolize_names: true)
expect(user_response[:errors][:email]).to include "is invalid"
end
Getting the tests to pass requires us to build the update action on the
users_controller.rb file as well as adding it to the routes.rb. As you
can see we have to much code duplicated, we’ll refactor our tests in Chapter 4
First we add the action the routes.rb file
.
.
.
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
# We are going to list our resources here
resources :users, :only => [:show, :create, :update]
end
.
.
.
48 CHAPTER 3. PRESENTING THE USERS
Then we implement the update action on the users controller and make
our tests pass (app/controllers/api/v1/users_controller.rb):
def update
user = User.find(params[:id])
if user.update(user_params)
render json: user, status: 200, location: [:api, user]
else
render json: { errors: user.errors }, status: 422
end
end
If we run our tests, we should now have all of our tests passing.
We commit the changes as we added a bunch of working code:
$ git add .
$ git commit -m "Adds update action the users controller"
[chapter3 d4422ff] Adds update action the users controller
3 files changed, 48 insertions(+), 1 deletion(-)
end
3.2. BUILDING USERS ENDPOINTS 49
As you can see the spec is very simple, as we only respond with a status
of 204 which stands for No Content, meaning that the server successfully
processed the request, but is not returning any content. We could also return a
200 status code, but I find more natural to respond with nothing in this case as
we are deleting a resource and a success response may be enough.
The implementation for the destroy action is fairly simple as well:
def destroy
user = User.find(params[:id])
user.destroy
head 204
end
Remember to add the destroy action to the user resources on the routes.rb
file:
.............
$ git add .
$ git commit -m "Adds destroy action to the users controller"
[chapter3 2034ddf] Adds destroy action to the users controller
3 files changed, 17 insertions(+), 1 deletion(-)
50 CHAPTER 3. PRESENTING THE USERS
This tool was developed with the intention to reduce the learning curve when
integrating new teammates to the project, or simply because is will just run on any
browser and offers, almost the same support as postman.
It uses well supported dependencies such as:
• Compass
• Furatto
• Simple Form
• Font Awesome
3.3. INTEGRATING SABISU 51
Even better you can customise to never add the attributes for creating a new
record, believe me, I have worked with models with up to 20 attributes and I’ve
had to add them manually, it really takes a lot of time.
You will know what I mean as we move forward with the gem integration.
$ bundle install
After installing the gems via bundler, we run the simple_form and
sabisu generators:
The commands above will generate two initialiser files, we only care about
the one related to sabisu located on config/initializers/sabisu_rails.rb.
If everything went smooth we just have to customise the sabisu engine
(Listing 3.13):
SabisuRails.setup do |config|
# HTTP methods
# config.http_methods = %w{ GET POST PUT DELETE PATCH }
# Layout configuration
# config.layout = "sabisu"
# Default resource
config.default_resource = :users
# Application name
# mattr_accessor :app_name
# @@app_name = Rails.application.class.parent_name
# Authentication
# mattr_accessor :authentication_username
# @@authentication_username = "admin"
# mattr_accessor :authentication_password
# @@authentication_password = "sekret"
end
You can customize it further to meet other requirements, but for now this
just works fine. Now let’s try the explorer now, by running rails server on
the console and open up the browser with the following url http://localhost:3000/sabisu_rails
(Figure 3.1)
If you don’t see the error message and instead a window for authentication
pops up, the credentials to access are admin for the username and sekret
3.3. INTEGRATING SABISU 53
for the password. These settings can be customized under the sabisu_rails
config file under config/initializers.
But wait a minute there is an error over there. this is something I have found
when using pow and httparty. For some reason URIs with underscore cannot
be parsed correctly so we get:
- the scheme http does not accept registry part: api.market_place_api.dev
(or bad hostname?) -
Trust me, I spent hours trying to solve this and wanted to point out this
issue on purpose as it might save you some cups of coffee. The solution is
fairly simple and is related to directory naming:
First move to the workspace directory or wherever the market_place_api
directory is:
$ cd ~/workspace/
$ mv market_place_api marketplaceapi
Yes you can do this through the UI, but I feel more comfortable working
through the terminal. Then we update the symbolic link we created on Sec-
tion 1.3.1 to point to the new directory:
$ cd ~/.pow
$ ln -s ~/workspace/marketplaceapi
54 CHAPTER 3. PRESENTING THE USERS
And just keep things organised we delete the old symbolic link:
$ rm market_place_api
config.base_api_uri = 'api.marketplaceapi.dev'
3.4 Conclusion
Oh you are here!, great job! I know it probably was a long way, but don’t give
up you are doing it great. Make sure you are understanding every piece of code,
3.4. CONCLUSION 55
things will get better, in Chapter 4 we will refactor our tests to clean our code
a bit and make it easy to extend the test suite more. So stay with me guys!
Feeling like want to share your achievement, I’ll be glad to read about it:
I just finished modeling the users for my api of Api on Rails tutorial by
@kurenn!
56 CHAPTER 3. PRESENTING THE USERS
Chapter 4
Refactoring tests
In Chapter 3 we manage to put together some user resources endpoints, if you
skip it, or simple missed it I highly recommend you take a look at it, it covers
the first test specs and an introduction to json responses.
You can clone the project until this point with:
In this chapter we’ll refactor our test specs by adding some helper methods,
remove the format param sent on every request and do it through headers, and
hopefully build more consistent and scalable test suite.
So let’ take a look to the users_controller_spec.rb file (Listing 4.1):
describe Api::V1::UsersController do
before(:each) { request.headers['Accept'] = "application/vnd.marketplace.v1" }
57
58 CHAPTER 4. REFACTORING TESTS
it "renders the json representation for the user record just created" do
user_response = JSON.parse(response.body, symbolize_names: true)
expect(user_response[:email]).to eql @user_attributes[:email]
end
it "renders the json errors on whye the user could not be created" do
user_response = JSON.parse(response.body, symbolize_names: true)
expect(user_response[:errors][:email]).to include "can't be blank"
end
it "renders the json errors on whye the user could not be created" do
user_response = JSON.parse(response.body, symbolize_names: true)
expect(user_response[:errors][:email]).to include "is invalid"
end
end
end
As you can see there is a lot of duplicated code, two big refactors here are:
So let’s add a method for handling the json response, but before we con-
tinue, and if you have been following the tutorial you may know that we are
creating a branch for each chapter, so let’s do that:
$ mkdir spec/support
$ touch spec/support/request_helpers.rb
It is time to extract the JSON.parse method into our own support method
(Listing 4.2):
We scope the method into some modules just to keep our code nice and
organised. The next step here is to update the users_controller_spec.rb
file to use the method. A quick example is presented below:
After that if we run our tests again, everything should be green again. So
let’s commit this before adding more code:
62 CHAPTER 4. REFACTORING TESTS
$ git add .
$ git commit -m "Refactors the json parse method"
[chapter4 26b7e51] Refactors the json parse method
3 files changed, 17 insertions(+), 7 deletions(-)
create mode 100644 spec/support/request_helpers.rb
describe Api::V1::UsersController do
# we concatenate the json format
before(:each) { request.headers['Accept'] = "application/vnd.marketplace.v1, #{Mime::JSON}" }
.
.
.
By adding this line, you can now remove all the format param we were
sending on each request and forget about it for the whole application, as long
as you include the Accept header with the json mime type.
Wait we are not over yet! We can add another header to our request that
will help us describe the data contained we are expecting from the server to
deliver. We can achieve this fairly easy by adding one more line specifying the
Content-Type header:
describe Api::V1::UsersController do
before(:each) { request.headers['Accept'] = "application/vnd.marketplace.v1, #{Mime::JSON}" }
# now we added this line
before(:each) { request.headers['Content-Type'] = Mime::JSON.to_s }
.
.
.
And again if we run our tests, we can see they are all nice and green:
4.3. REFACTOR BEFORE ACTIONS 63
before(:each) do
request.headers['Accept'] = "application/vnd.marketplace.v1, #{Mime::JSON}"
request.headers['Content-Type'] = Mime::JSON.to_s
end
This is good, but not good enough, because we will have to add this 5
lines of code for each file, and if for some reason we are changing let’s say
the response type to xml, well you do the math. But don’t worry I provide a
solution which will solve all these problems.
First of all we have to extend our request_helpers.rb file to include
another module, which I named HeadersHelpers and which will have the
necessary methods to handle these custom headers(Listing 4.4)
def include_default_accept_headers
api_header
api_response_format
end
end
end
As you can see I broke the calls into 2 methods, one for setting the api
header and the other one for setting the response format. Also and for conve-
nience I wrote a method (include_default_accept_headers) for calling
those two.
And now to call this method before each of our test cases we can add the
before hook on the Rspec.configure block at spec_helper.rb 4.5 file, and make
sure we specify the type to :controller, as we don’t to run this on unit tests.
RSpec.configure do |config|
.
.
.
config.include Request::HeadersHelpers, :type => :controller
After adding this lines, we can remove the before hooks on the users_controller_sp
file and check that our tests are still passing.
You can review a full version of the spec_helper.rb file below (List-
ing 4.5):
4.3. REFACTOR BEFORE ACTIONS 65
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
RSpec.configure do |config|
# ## Mock Framework
#
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
#
# config.mock_with :mocha
# config.mock_with :flexmock
# config.mock_with :rr
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
end
Well now I do feel satisfied with the code, let’s commit the changes:
$ git add .
$ git commit -am "Refactors test headers for each request"
[chapter4 ae10fe9] Refactors test headers for each request
3 files changed, 21 insertions(+), 5 deletions(-)
Remember you can review the code up to this point at the github repository.
4.4 Conclusion
Nice job on finishing this chapter, although it was a short one it was a crucial
step as this will help us write better and faster tests. On Chapter 5 we will add
the authentication mechanism we’ll be using across the application as well as
limiting the access for certain actions.
I’ll be really excited to hear about you:
I just finished refactoring the test suite for my api of Api on Rails tutorial
by @kurenn!
Chapter 5
Authenticating users
It’s been a long way since you started, I hope you are enjoying this trip as much
as me. On Chapter 4 we refactor our test suite and since we did not add much
code, it didn’t take to much. If you skipped that chapter I recommend you read
it, as we are going to be using some of the methods in the chapters to come.
You can clone the project up to this point:
In this chapter things will get very interesting, and that is because we are
going to set up our authentication mechanism. In my opinion this is going to
be one of the most interesting chapters, as we will introduce a lot of new terms
and you will end with a simple but powerful authentication system. Don’t feel
panic we will get to that.
First things first, and as usual when starting a new chapter, we will create a
new branch:
67
68 CHAPTER 5. AUTHENTICATING USERS
1. The client request for sessions resource with the corresponding cre-
dentials, usually email and password.
2. The server returns the user resource along with its corresponding au-
thentication token
3. Every page that requires authentication, the client has to send that authentication
token
Of course this is not the only 3-step to follow, and even on step 2 you
might think, well do I really need to respond with the entire user or just the
authentication token, I would say, it really depends on you, but I like to
return the entire user, this way I can map it right away on my client and save
another possible request from being placed.
In this section and the next we will be focusing on building a Sessions
controller along with its corresponding actions. We’ll then complete the request
flow by adding the necessary authorization access.
Then we run the migrations to add the field and prepare the test database:
Now it would be a good time to add some response and uniqueness tests to
our user model spec (Listing 5.1)
We then move to the user.rb file and add the necessary code to make our
tests pass:
70 CHAPTER 5. AUTHENTICATING USERS
describe "#generate_authentication_token!" do
it "generates a unique token" do
Devise.stub(:friendly_token).and_return("auniquetoken123")
@user.generate_authentication_token!
expect(@user.auth_token).to eql "auniquetoken123"
end
def generate_authentication_token!
begin
self.auth_token = Devise.friendly_token
end while self.class.exists?(auth_token: auth_token)
end
before_create :generate_authentication_token!
Then we need to move the files into the api/v1 directory, for both on the
app and spec folders:
$ mv app/controllers/sessions_controller.rb app/controllers/api/v1
$ mv spec/controllers/sessions_controller_spec.rb spec/controllers/api/v1
After moving the files we have to update them to meet the directory struc-
ture we currently have as shown on Listing 5.3 and Listing 5.4.
end
describe Api::V1::SessionsController do
end
Sign in success
Our first stop will be the create action, but first and as usual let’s generate our
tests (Listing 5.5)
require 'spec_helper'
describe Api::V1::SessionsController do
before(:each) do
@user = FactoryGirl.create :user
end
before(:each) do
credentials = { email: @user.email, password: "12345678" }
post :create, { session: credentials }
end
before(:each) do
credentials = { email: @user.email, password: "invalidpassword" }
post :create, { session: credentials }
end
The tests are pretty straightforward we simply return the user in json for-
mat if the credentials are correct, but if not we just send a json with an error
message. Next we need to implement the code to make our tests be green (List-
ing 5.6). But before that we will add the end points to our route.rb file (both
the create and destroy end point).
74 CHAPTER 5. AUTHENTICATING USERS
.
.
.
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
# We are going to list our resources here
resources :users, :only => [:show, :create, :update, :destroy]
resources :sessions, :only => [:create, :destroy]
end
.
.
.
def create
user_password = params[:session][:password]
user_email = params[:session][:email]
user = user_email.present? && User.find_by(email: user_email)
if user.valid_password? user_password
sign_in user, store: false
user.generate_authentication_token!
user.save
render json: user, status: 200, location: [:api, user]
else
render json: { errors: "Invalid email or password" }, status: 422
end
end
end
Before we run our tests, it is necessary to add the devise test helpers in
the spec_helper.rb file:
.
.
.
#Including to test requests
config.include Request::JsonHelpers, :type => :controller
config.include Request::HeadersHelpers, :type => :controller
config.include Devise::TestHelpers, :type => :controller
.
.
.
5.1. STATELESS AND SIGN IN FAILURE 75
$ git add .
$ git commit -m "Adds sessions controller create action"
Sign out
We currently have the sign in end point for the api, now it is time to build a
sign out url, and you might wonder why, since we are not handling sessions
and there is nothing to destroy. In this case we are going to update the authen-
tication token so the last one becomes useless and cannot be used again.
It is actually not necessary to include this end point, but I do like to include
it to expire the authentication tokens
As usual we start with the tests (Listing 5.7)
before(:each) do
@user = FactoryGirl.create :user
sign_in @user, store: false
delete :destroy, id: @user.auth_token
end
end
As you can see the test is super simple, now we just need to implement
the necessary code to make our tests pass (Listing 5.8)
76 CHAPTER 5. AUTHENTICATING USERS
$ git add .
$ git commit -m "Adds destroy session action added"
[chapter5 cb8b47a] Adds destroy session action added
3 files changed, 21 insertions(+), 1 deletion(-)
$ touch app/controllers/concerns/authenticable.rb
$ mkdir spec/controllers/concerns
$ touch spec/controllers/concerns/authenticable_spec.rb
As usual we start by writing our tests, in this case for our current_user
method, which will fetch a user by the authentication token ok the Authorization
header.(Listing 5.9)
class Authentication
include Authenticable
end
describe Authenticable do
let(:authentication) { Authentication.new }
subject { authentication }
describe "#current_user" do
before do
@user = FactoryGirl.create :user
request.headers["Authorization"] = @user.auth_token
authentication.stub(:request).and_return(request)
end
78 CHAPTER 5. AUTHENTICATING USERS
Now we just need to include the Authenticable module into the ApplicationContr
5.3. AUTHENTICATE WITH TOKEN 79
include Authenticable
end
$ git add .
$ git commit -m "Adds authenticable module for managing authentication methods"
[chapter5 1d049c1] Adds authenticable module for managing authentication methods
3 files changed, 30 insertions(+)
create mode 100644 app/controllers/concerns/authenticable.rb
create mode 100644 spec/controllers/concerns/authenticable_spec.rb
.
describe "#authenticate_with_token" do
before do
@user = FactoryGirl.create :user
authentication.stub(:current_user).and_return(nil)
response.stub(:response_code).and_return(401)
response.stub(:body).and_return({"errors" => "Not authenticated"}.to_json)
authentication.stub(:response).and_return(response)
end
As you can see we are using the Authentication class again and stub-
bing the request and response for handling the expected answer from the
server. Now it is time to implement the code to make our tests pass (List-
ing 5.12).
end
$ git commit -m "Adds the authenticate with token method to handle access to actions"
[chapter5 99e869d] Adds the authenticate with token method to handle access to actions
2 files changed, 22 insertions(+)
if user.update(user_params)
render json: user, status: 200, location: [:api, user]
else
render json: { errors: user.errors }, status: 422
end
end
.
.
.
And as you might expect, if we run our users controller specs they should
fail:
82 CHAPTER 5. AUTHENTICATING USERS
Failures:
.
.
.
Now the tests should be all green. But wait something does not feel quite
right isn’t it?, we can refactor the line we just added and put it on the HeadersHelpers
module we build on Chapter 4. Listing 5.14
Now each time we need to have the current_user on our specs we simply
call the api_authorization_header method. I’ll let you do that with the
users_controller_spec.rb for the update spec.
5.4. AUTHORIZE ACTIONS 83
For the destroy action we will do the same, because we just have to make
sure a user is capable to self destroy Listing 5.15
Now for the spec file and as mentioned before, we just need to add the
api_authorization_header:
end
We should have all of our tests passing. The last step for this section consist
on adding the corresponding authorization access for these last 2 actions.
It is common to just prevent the actions on which the user is performing
actions on the record itself, in this case the destroy and update action
On the users_controller.rb we have to filter some these actions to
prevent the access Listing 5.16
Our tests should still be passing. And from now on everytime we want to
prevent any action from being trigger, we simply add the authenticate_with_token!
method on a before_action hook.
Let’s just commit this:
$ git add .
$ git commit -m "Adds authorization for the users controller"
[chapter5 7c19c79] Adds authorization for the users controller
3 files changed, 10 insertions(+), 4 deletions(-)
Lastly but not least we will finish the chapter by refactoring the authenticate_with_t
method, it is really a small enhancement, but it will make the method more de-
scriptive. You’ll see what I mean in a minute(Listing 5.18), but first things first,
let’s add some specs.
it { should be_user_signed_in }
end
it { should_not be_user_signed_in }
end
end
end
As you can see we added two simple specs to know whether the user is
signed in or not, and as I mentioned early it is just for visual clarity. But let’s
keep going and add the implementation. (Listing 5.18)
def user_signed_in?
current_user.present?
end
end
$ git add .
$ git commit -m "Adds user_signed_in? method to know whether the user is logged in or not"
5.5 Conclusion
Yei! you made it! you are half way done! keep up the good work, this chap-
ter was a long and hard one but it is a great step forward on setting a solid
mechanism for handling user authentication and we even scratch the surface
for simple authorization rules.
In the next chapter we will be focusing on customizing the json output
for the user with active_model_serializers gem and adding a product
model to the equation by giving the user the ability to create a product and
publish it for sale.
In the mean time let me know how you doing and shout me a tweet:
I can authenticate users for my api thanks to Api on Rails tutorial by
@kurenn!
Chapter 6
User products
On Chapter 5 we implemented the authentication mechanism we’ll be using
all along the app. Right now we have a very simple implementation of the
user model but the moment of truth has come where we will customise the
json output but also add a second resource: user products. These are the items
that the user will be selling in the app, and by consequence will be directly
associated. If you are familiar with Rails you may already know what I’m
talking about, but for those who doesn’t, we will associated the User to the
Product model using the has_many and belongs_to active record methods.
Recall (Figure 2.1).
In this chapter we will build the Product model from the ground up, asso-
ciate it with the user and create the necessary end points for any client to access
the information.
You can clone the project up to this point:
Before we start and as usual when starting new features, we need to branch
it out:
87
88 CHAPTER 6. USER PRODUCTS
As you may notice we also added an index option to the user_id attribute,
this is a good practice when using association keys, as it optimizes the query to
a database level. It is not compulsory that you do that but I highly recommend
it.
The migration file should look like this:
t.timestamps
end
add_index :products, :user_id
end
end
Take note that we set some default values for all of the attributes except the
user_id, this way we keep a high consistency level on our database as we
don’t deal with many NULL values.
Next we will add some basic tests to the Product model. We will just make
sure the object responds to the fields we added, as shown in Listing 6.1:
describe Product do
let(:product) { FactoryGirl.build :product }
subject { product }
it { should respond_to(:title) }
it { should respond_to(:price) }
it { should respond_to(:published) }
it { should respond_to(:user_id) }
it { should not_be_published }
end
Remember to migrate the database and prepare the test one so we get out
tests green:
$ rake db:migrate
$ rake db:test:prepare
Although our tests are passing, we need to do some ground work for the
product factory, as for now is all hardcoded. As you recall we have been using
Faker to fake the values for our tests models (Listing 3.3), now it is time to do
the same with the product model. See Listing 6.2
Now each product we create will look a bit more like a real product. We
still need to work on the user_id as is hardcoded, but we will get to that on
Section 6.1.3.
.
it { should validate_presence_of :title }
it { should validate_presence_of :price }
it { should validate_numericality_of(:price).is_greater_than_or_equal_to(0) }
it { should validate_presence_of :user_id }
end
end
We have a bunch of good quality code, let’s commit it and keep moving:
$ git add .
$ git commit -m "Adds product model bare bones along with some validations"
[chapter6 f774173] Adds product model bare bones along with some validations
5 files changed, 58 insertions(+), 1 deletion(-)
create mode 100644 app/models/product.rb
create mode 100644 db/migrate/20140718160927_create_products.rb
create mode 100644 spec/factories/products.rb
create mode 100644 spec/models/product_spec.rb
FactoryGirl.define do
factory :product do
title { FFaker::Product.product_name }
price { rand() * 100 }
published false
92 CHAPTER 6. USER PRODUCTS
user
end
end
As you can see we just rename the user_id attribute to user and we did
not specify a value, as FactoryGirl is smart enough to create a user object for
every product and associate them automatically. Now we need to add some
tests for the association. See Listing 6.4
end
As you can see the test we added is very simple, thanks to the power of
shoulda-matchers. We continue with the implementation now:
end
Remember to run the test we added just to make sure everything is all right:
Currently we only have one part of the association, but as you may be won-
dering already we have to add a has_many association to the user model.
First we add the test on the user_spec.rb file:
6.1. PRODUCT MODEL 93
describe User do
.
.
.
it { should have_many(:products) }
.
.
.
end
has_many :products
.
.
.
end
Now if we run the user specs, they should be all nice and green:
Dependency destroy
Something I’ve seen in other developers code when working with associa-
tions, is that they forget about dependency destruction between models. What I
mean by this is that if a user is destroyed, the user’s products in this case should
be destroyed as well.
So to test this interaction between models, we need a user with a bunch of
products, then we destroy that user expecting the products disappear along with
it. A simple implementation would look like this:
94 CHAPTER 6. USER PRODUCTS
products = user.products
user.destroy
products.each do |product|
expect(Product.find(product.id)).to raise_error ActiveRecord::RecordNotFound
end
We first save the products into a variable for later access, then we destroy
the user and loop through the products variable expecting each of the products
to raise an exception. Putting everything together should look like the code in
Listing 6.5:
subject { @user }
.
.
.
describe "#products association" do
before do
@user.save
3.times { FactoryGirl.create :product, user: @user }
end
The necessary code to make the code on Listing 6.5 to pass is just an option
on the has_many association method:
6.2. PRODUCTS ENDPOINTS 95
$ git add .
$ git commit -m "Finishes modeling the product model along with user associations"
[chapter6 13795aa] Finishes modeling the product model along with user associations
5 files changed, 26 insertions(+), 2 deletions(-)
The command above will generate a bunch of files ready to start working,
what I mean by this is that it will generate the controller and specs files already
scoped to the version 1 of the api. Listing 6.6
96 CHAPTER 6. USER PRODUCTS
And the spec file should look like the one on Listing 6.7
describe Api::V1::ProductsController do
end
As a warmup we will start nice and easy by building the show action for
the product.
end
end
def show
respond_with Product.find(params[:id])
end
end
Wait!, don’t run the tests yet, remember we need to add the resource to the
routes.rb file:
As you may notice already the specs and implementation are very simple,
actually they behave the same as the users, recall Listing 3.6
98 CHAPTER 6. USER PRODUCTS
Let’s move into the implementation, which for now is going to be a sad all
class method. Listin 6.10
def index
respond_with Product.all
end
.
.
.
end
6.2. PRODUCTS ENDPOINTS 99
In next chapters we will improve this endpoint, and give it the ability to
receive parameters to filter them.
SabisuRails.setup do |config|
.
.
.
config.resources = [:users, :products]
.
.
.
end
If you run the rails server from the command line, and visit http://localhost:3000/sabisu_r
the product resource should be selected and the screen should look like Fig-
ure 6.1
You can create a bunch of products through the console using Factory-
Girl and see a reasonable response.
We are done for now with the public product endpoints, in the sections to
come we will focus on building the actions that require a user to be logged in
to access them. Said that we are committing this changes and continue.
$ git add .
$ git commit -am "Adds public product endpoints(show, index)"
[chapter6 185adcb] Adds public product endpoints(show, index)
100 CHAPTER 6. USER PRODUCTS
.
describe "POST #create" do
context "when is successfully created" do
before(:each) do
user = FactoryGirl.create :user
@product_attributes = FactoryGirl.attributes_for :product
api_authorization_header user.auth_token
post :create, { user_id: user.id, product: @product_attributes }
end
it "renders the json representation for the product record just created" do
product_response = json_response
expect(product_response[:title]).to eql @product_attributes[:title]
end
it "renders the json errors on whye the user could not be created" do
product_response = json_response
expect(product_response[:errors][:price]).to include "is not a number"
end
Wow!, we added a bunch of code, but if you recall from Section 3.2.2, the
spec actually looks the same as the user create action but with minor changes.
Remember we have this endpoint nested so we need to make sure we send the
user_id param on each request, as you can see on:
102 CHAPTER 6. USER PRODUCTS
This way we can fetch the user and create the product for that specific
user. But wait there is more, if we take this approach we will have to in-
crement the scope of our authorization mechanism, because we have to fetch
the user from the user_id param. Well in this case and if you remember
we built the logic to get the user from the authorization header and as-
signed it a current_user method. This is rapidly fixable, by just adding the
authorization header into the request, and fetch that user from it, so let’s
do that. Listing 6.12
private
def product_params
params.require(:product).permit(:title, :price, :published)
end
end
As you can see we are protecting the create action with the authenticate_with_toke
method, and on the create action we are building the product in relation to
the current_user.
By this point you may be asking yourself, well is it really necessary to nest
the action?, because by the end of the day we don’t really use the user_id
6.2. PRODUCTS ENDPOINTS 103
from the uri pattern. In my opinion you are totally right, my only argument
here is that with this approach the endpoint is way more descriptive from the
outside, as we are telling the developers that in order to create a product we
need a user.
So it is really up to you how you want to organize your resources and expose
them to the world, my way is not the only one and it does not mean is the correct
one either, in fact I encourage you to play around with different approaches and
choose the one that fills your eye.
One last thing before you run your tests, just the necessary route:
Now if you run the tests now, they should be all green:
Before we start dropping some tests, I just want to clarify that similarly to
the create action we will scope the product to the current_user, in this
case we want to make sure the product we are updating, actually belongs to
the user, so we will fetch that product from the user.products association
provided by rails.
First we add some specs, Listing 6.13
end
it "renders the json errors on whye the user could not be created" do
product_response = json_response
expect(product_response[:errors][:price]).to include "is not a number"
end
The tests may look complex, but take a second peek, they are almost the
same as the users on Listing 3.11, the only difference here is the nested routes
as we saw on Section 6.2.4, which in this case we need to send the user_id
as a parameter.
Now let’s implement the code to make our tests pass Listing 6.14:
Now we simply add the necessary code to make the tests pass, see List-
ing 6.16:
As you can see the three-line implementation does the job, we can run the
tests to make sure everything is good, and after that we will commit the changes
as we added a bunch of new code. Also make sure you hook this action to the
before_action callback as with the update action. Listing 6.14
$ git add .
$ git commit -m "Adds the products create, update and destroy action nested on the user resource
[chapter6 73057d2] Adds the products create, update and destroy action nested on the user resour
3 files changed, 124 insertions(+), 1 deletion(-)
108 CHAPTER 6. USER PRODUCTS
$ rails console
.
.
.
group :development do
gem 'sqlite3'
end
group :test do
6.3. POPULATING THE DATABASE 109
gem "rspec-rails"
gem "factory_girl_rails"
gem 'ffaker'
gem "shoulda-matchers"
end
.
.
.
You see where the problem is?. If you pay attention you will notice that the
factory_girl_rails gem is only available for the test environment, but no
for the development one, which is what we need. This can be fix really fast:
group :development do
gem 'sqlite3'
end
group :test do
gem "rspec-rails"
gem "shoulda-matchers"
end
Notice the we moved the ffaker gem to the shared group as we use it
inside the factories we describe earlier. Now just run the bundle command to
update the libraries. Then build the products you want like so:
$ rails console
Loading development environment (Rails 4.0.2)
2.1.0 :001 > 20.times { FactoryGirl.create :product }
.
.
.
From now on, you will be able to create any object from factories, such as
users, products, orders, etc.
So let’s commit this tiny change:
110 CHAPTER 6. USER PRODUCTS
$ git add .
$ git commit -m "Updates test environment factory gems to work on development"
[chapter6 6da3f8f] Updates test environment factory gems to work on development
1 file changed, 5 insertions(+), 2 deletions(-)
If you check the output now for the products using sabisu, it should look
like the Figure 6.2. It does not look nice isn’t it?, we have to make some
customisation to the output using active_model_serializers.
6.4 Conclusion
On the next chapter we, will focus on customising the output from the user
and product models using the active model serializers gem which will help
us to easily filter the attributes to display, or handle associations as embebed
objects for example.
I hope you have enjoyed this chapter, it is a long one but the code we put
together is an excellent base for the core app. In the mean time, tell me how
are you doing, I’ll be glad to hear from you:
I just finished modeling the products for my api of Api on Rails tutorial
by @kurenn!
Chapter 7
},
{
"id": 2,
"title": "Plasma TV",
}
111
112 CHAPTER 7. JSON WITH ACTIVE MODEL SERIALIZERS
.
.
.
gem 'active_model_serializers', git: '[email protected]:rails-api/active_model_serializers.git', br
.
.
.
Run the bundle install command to install the gem, and that is it, you
should be all set to continue with the tutorial.
By now we should have some failing tests. Go ahead and try it:
114 CHAPTER 7. JSON WITH ACTIVE MODEL SERIALIZERS
If you take a quick look at the users_controller, you may have some-
thing like Listing 3.7 for the show action, and as it is mention on the gem
documentation, Box 7.1
In this case, Rails will look for a serializer named PostSerializer, and
if it exists, use it to serialize the Post.
This also works with respond_with, which uses to_json under the
hood. Also note that any options passed to render :json will be passed
to your serializer and available as @options inside.
This means that no matter if we are using the render json method or
respond_with, from now on Rails will look for the corresponding serialiser first.
Now back to the specs, you can see that for some reason the response it’s
not quite what we are expecting, and that is because the gem encapsulates the
model into a javascript object with the model name as the root, in this case
user.
So in order to make the tests pass we just need to add the attributes to seri-
alise into the user_serializer.rb and update the users_controller_spec.rb
file:
7.2. SERIALISE THE USER MODEL 115
require 'spec_helper'
describe Api::V1::UsersController do
describe "GET #show" do
before(:each) do
@user = FactoryGirl.create :user
get :show, id: @user.id
end
it "renders the json representation for the user record just created" do
user_response = json_response[:user]
expect(user_response[:email]).to eql @user_attributes[:email]
end
end
it "renders the json errors on whye the user could not be created" do
user_response = json_response
expect(user_response[:errors][:email]).to include "can't be blank"
end
it "renders the json errors on whye the user could not be created" do
user_response = json_response
expect(user_response[:errors][:email]).to include "is invalid"
end
end
end
If you pay enough attention and as I mentioned before the gem adds a root
for the json object that corresponds to the serialised object name, in this case
user. You can check it out with sabisu.
Now if you run the tests now, they should be all green:
$ git add .
$ git commit -am "Adds user serializer for customizing the json output"
[chapter7 827b1f9] Adds user serializer for customizing the json output
2 files changed, 6 insertions(+), 3 deletions(-)
create mode 100644 app/serializers/user_serializer.rb
We can also test the serialiser objects, as shown on the documentation, but
I’ll let that to you to decide wheter or not to test.
Now let’s add the attributes to serialize for the product, just as we did it
with the user back in Section 7.2:
Failures:
3) Api::V1::ProductsController POST #create when is successfully created renders the json repr
Failure/Error: expect(product_response[:title]).to eql @product_attributes[:title]
.
.
.
it "returns the information about a reporter on a hash" do
product_response = json_response[:product]
expect(product_response[:title]).to eql @product.title
end
.
.
.
Then we jump to line 86 and make the same change as the last one:
.
.
.
it "renders the json representation for the updated user" do
product_response = json_response[:product]
expect(product_response[:title]).to eql "An expensive TV"
end
.
.
.
Lastly we jump back to line 44, and yeah we make the same little change
we’ve been doing so far:
.
.
.
it "renders the json representation for the product record just created" do
product_response = json_response[:product]
expect(product_response[:title]).to eql @product_attributes[:title]
end
.
.
.
...............
If you feel anxious on how the json outputs are right now, remember you
can always use sabisu and play around with it.
Let’s commit the changes and move on onto Section 7.4:
$ git add .
$ git commit -a "Adds product serializer for custom json output"
[chapter7 f8834ec] Adds product serializer for custom json output
4 files changed, 8 insertions(+), 5 deletions(-)
create mode 100644 app/serializers/product_serializer.rb
7.4 Sessions
If you run all of your test suite, you may notice that we have a failing test on
the sessions_controlles_spec.rb file:
Failures:
1) Api::V1::SessionsController POST #create when the credentials are correct returns the user
Failure/Error: expect(json_response[:auth_token]).to eql @user.auth_token
expected: "z-zkCoks3xycLAUyBXzf"
got: nil
This is because we change the output format for the json response, and as
we have been doing it so far we have to update the spec as shown on Listing 7.3.
7.5. SERIALIZING ASSOCIATIONS 121
Now that we have everything nice and green let’s commit the changes:
$ git add .
$ git commit -am "Fixes the sessions controller spec"
[chapter7 6342601] Fixes the sessions controller spec
2 files changed, 2 insertions(+), 2 deletions(-)
.
.
has_many :products, dependent: :destroy
.
.
.
end
Image an scenario where you are fetching the products from the api, but in this
case you need to display some of the user info.
One possible solution to this would be to add the user_id attribute to the
product_serializer so we can fetch the corresponding user later. This might
sound like a good idea, but if you care about performance, or your database trans-
actions are not fast enough, you should reconsider this approach, because you have
to realize that for every product you fetch, you’ll have to request its corresponding
user.
When facing this problem I’ve come with two possible alternatives:
7.5. SERIALIZING ASSOCIATIONS 123
• One good solution in my opinion is to embed the user ids related to the
products into a meta attribute, so we have a json output like:
{
"meta": { "user_ids": [1,2,3] },
"products": [
.
.
.
]
}
This might need some further configuration on the user’s endpoint, so the client
can fetch those users from those user_ids.
• Another solution and the one which I’ll be using here, is to embed the user
object into de product object, this can make the first request a bit slower, but
this way the client does not need to make another extra request. An example
of the expected output is presented below:
"products":
[
{
"id": 1,
"title": "Digital Portable System",
"price": "25.0277354166289",
"published": false,
"user": {
"id": 2,
"email": "[email protected]",
"created_at": "2014-07-29T03:52:07.432Z",
"updated_at": "2014-07-29T03:52:07.432Z",
"auth_token": "Xbnzbf3YkquUrF_1bNkZ"
}
}
.
.
.
]
124 CHAPTER 7. JSON WITH ACTIVE MODEL SERIALIZERS
So according to Box 7.2 we’ll be embeding the user object into the product,
let’s start by adding some tests. We will just modify the show and index
endpoints spec. Listing 7.6
7.5. SERIALIZING ASSOCIATIONS 125
The implementation is really easy, we just need to add one line to the prod-
uct serializer:
$ git add .
$ git commit -m "Embeds the user into the product json output"
[chapter7 e99ddf4] Embeds the user into the product json output
2 files changed, 13 insertions(+)
before(:each) do
@user = FactoryGirl.create :user
get :show, id: @user.id
end
.
.
.
it "has the product ids as an embeded object" do
user_response = json_response[:user]
expect(user_response[:product_ids]).to eql []
end
end
has_many :products
end
As you can see from Listing 7.9 we just wrapped the index action into two
separate contexts, one which will recevive the product_ids, and the old one
we had which does not. Let’s add the necessary code to make the tests pass:
As you can see the implementation is super simple, we simply just fetch the
products from the product_ids params in case they are present, otherwise we
just fetch all of them. Let’s make sure the tests are passing:
$ git commit -m "Embeds the products_ids into the user serialiser and fetches the correct produc
[chapter7 57d052b] Embeds the products_ids into the user serialiser and fetches the correct prod
4 files changed, 41 insertions(+), 10 deletions(-)
let any client filter the results. This section is optional as it’s not going to have
impact on any of the modules in the app, but if you want to practice more with
TDD and keep the brain warm I recommend you complete this last step.
I’ve been using Ransack to build advance search forms extremely fast, but
as this is an education tool (or at least I consider it), and the search we’ll be
performing is really simple, I think we can build a simple search engine, we
just need to consider the criteria by which we are going to filter the attributes.
Hold tight to your seats this is going to be a rough ride.
We will filter the products by the following criteria:
• By a title pattern
• By price
• Sort by creation
This may sound short and easy but believe me it will give you a headache
if you don’t plan it.
7.6.1 By keyword
We will create a scope to find the records which match a particular pattern of
characters, let’s called it filter_by_title, let’s add some specs first:
end
7.6. SEARCHING PRODUCTS 131
The caveat in here is to make sure no matter the case of the title sent we
have to sanitize it to any case in order to make the apropiate comparison, in this
case we’ll use the lower case approach. Let’s implement the necessary code:
end
7.6.2 By price
In order to filter by price, things can get a little bit tricky, but actually it is very
easy, we will break the logic to filter by price into two different methods, one
132 CHAPTER 7. JSON WITH ACTIVE MODEL SERIALIZERS
which will fetch the products greater than the price received and the other one
to look for the ones under that price. By doing this we keep everything really
flexible and we can easily test the scopes.
Let’s start by building the above_or_equal_to_price scope specs:
end
You can now imagine how the opposite method will behave, let’s add the
specs:
end
134 CHAPTER 7. JSON WITH ACTIVE MODEL SERIALIZERS
For our sake let’s run the tests and verify evertyhing is nice and green:
As you can see we have not gotten in a lot of trouble, let’s just add another
scope, to sort the records by date of last update, this is because in case the
propietary of the product decides to update some of the data, the client always
fetches the most updated records.
end
Now it would be a good time to commit the changes as we are done adding
scopes:
context "when title 'videogame' and '100' a min price are set" do
it "returns an empty array" do
search_hash = { keyword: "videogame", min_price: 100 }
expect(Product.search(search_hash)).to be_empty
end
end
context "when title 'tv', '150' as max price, and '50' as min price are set" do
it "returns the product1" do
search_hash = { keyword: "tv", min_price: 50, max_price: 150 }
expect(Product.search(search_hash)).to match_array([@product1])
end
end
products
end
end
We can run the whole test suite, to make sure the app is healthy up to this
point:
7.7 Conclusion
On chapters to come, we will start building the Order model, associate it with
users and products, which so far and thanks to the active_model_serializers
gem, it’s been easy.
This was a long chapter, you can sit back, rest and look how far we got. I
hope you are enjoying what you got until now, it will get better. We still have a
lot of topics to cover one of them is optimization and caching.
I just finished chapter 7 of Api on Rails tutorial by @kurenn!
Chapter 8
Placing Orders
Back in Chapter 7 we handle associations between the product and user models,
and how to serialize them in order to scale fast and easy. Now it is time to
start placing orders which is going to be a more complex situation, because we
will handle associations between 3 models and we have to be smart enough to
handle the json output we are delivering.
In this chapter we will make several things which I list below:
2. Handle json output association between the order user and product mod-
els
So now that we have everything clear, we can get our hands dirty. You can
clone the project up to this point with:
139
140 CHAPTER 8. PLACING ORDERS
The command above will generate the order model, but I’m taking advan-
tage of the references method to create the corresponding foreign key for
the order to belong to a user, it also adds the belongs_to directive into the
order model. Let’s migrate the database and jump into the order_spec.rb
file.
$ rake db:migrate
== CreateOrders: migrating ===================================================
-- create_table(:orders)
-> 0.0039s
== CreateOrders: migrated (0.0040s) ==========================================
describe Order do
let(:order) { FactoryGirl.build :order }
subject { order }
8.1. MODELING THE ORDER 141
it { should respond_to(:total) }
it { should respond_to(:user_id) }
describe Order do
let(:order) { FactoryGirl.build :order }
subject { order }
.
.
.
it { should have_many(:placements) }
it { should have_many(:products).through(:placements) }
end
describe Product do
.
.
.
it { should have_many(:placements) }
it { should have_many(:orders).through(:placements) }
.
.
.
end
describe Placement do
let(:placement) { FactoryGirl.build :placement }
subject { placement }
If you have been following the tutorial so far, the implementation is already
there, because of the references type we pass on the model command gen-
erator. We should add the inverse option to the placement model for each
belongs_to call. This gives a little boost when referencing the parent object.
Let’s run the models spec and make sure everything is green:
Now that everything is nice and green, let’s commit the changes and con-
tinue with Section 8.3
$ git add .
$ git commit -m "Associates products and orders with a placements model"
[chapter8 2bd331d] Associates products and orders with a placements model
11 files changed, 105 insertions(+), 1 deletion(-)
create mode 100644 app/models/order.rb
create mode 100644 app/models/placement.rb
create mode 100644 db/migrate/20140826170754_create_orders.rb
create mode 100644 db/migrate/20140826173533_create_placements.rb
create mode 100644 spec/factories/orders.rb
create mode 100644 spec/factories/placements.rb
create mode 100644 spec/models/order_spec.rb
create mode 100644 spec/models/placement_spec.rb
.
.
.
#line 20
it { should have_many(:products) }
it { should have_many(:orders) }
.
.
.
.
.
.
#line 9
has_many :products, dependent: :destroy
has_many :orders, dependent: :destroy
.
.
.
You can run the tests for both files, and they should be all nice and green:
$ git add .
$ git commit -m 'Adds user order has many relation'
Let’s start with the index action, so first we have to create the orders con-
troller.
Up to this point and before start typing some code, we have to ask our-
selves, should I leave my order endpoints nested into the UsersController,
or should I isolate them, and the answer is really simple. I would say it depends
on how much information in this case in particular you want to expose to the
developer, not from a json output point of view, but from the URI format.
8.3. EXPOSING THE ORDER MODEL 147
I’ll nest the routes, because I like to give this type of information to the
developers, as I think it gives more context to the request itself.
Let’s start by dropping some tests:
end
If we run the test suite now, as you may expect, both tests will fail, because
have not even set the correct routes, nor the action. So let’s start by adding the
routes:
.
.
.
#line 9
resources :users, :only => [:show, :create, :update, :destroy] do
resources :products, :only => [:create, :update, :destroy]
resources :orders, :only => [:index]
end
.
.
.
def index
respond_with current_user.orders
end
end
$ git add .
$ git commit -m "Adds index orders controller action nested into the users resources"
.
#line 20
describe "GET #show" do
before(:each) do
current_user = FactoryGirl.create :user
api_authorization_header current_user.auth_token
@order = FactoryGirl.create :order, user: current_user
get :show, user_id: current_user.id, id: @order.id
end
def show
respond_with current_user.orders.find(params[:id])
end
Failures:
1) Api::V1::OrdersController GET #show returns the user order record matching the id
Failure/Error: expect(order_response[:id]).to eql @order.id
NoMethodError:
undefined method `[]' for nil:NilClass
150 CHAPTER 8. PLACING ORDERS
Failed examples:
Couln’t wait?, well the answer is that we are expecting some format from
the API, which is given by ActiveModelSerializers, which in this case
we have not created an order serializer. This is fairly easy to fix:
And just by adding the serializer, our tests should be all green:
We will leave the order serializer as it is for now, but don’t worry we will
customize it later.
Let’s commit the changes and move onto the create order action:
$ git add .
$ g commit -m "Adds the show action for order"
[chapter8 9038c1d] Adds the show action for order
4 files changed, 23 insertions(+), 1 deletion(-)
create mode 100644 app/serializers/order_serializer.rb
8.3. EXPOSING THE ORDER MODEL 151
As you can see we are creating a order_params variable with the order
data, can you see the problem here?, if not, I’ll explain it later, let’s just add the
necessary code to make this test pass.
First we need to add the action to the resources on the routes file:
if order.save
render json: order, status: 201, location: [:api, current_user, order]
else
render json: { errors: order.errors }, status: 422
end
end
private
def order_params
params.require(:order).permit(:total, :user_id, :product_ids => [])
end
6 examples, 0 failures
Just before you run your tests, we need to update the order factory, just to
make it more useful:
FactoryGirl.define do
factory :order do
user
total 0
end
end
before_validation :set_total!
At this point, we are making sure the total is always present and bigger or
equal to zero, meaning we can remove those validations and remove the specs.
I’ll wait.
Our tests should be passing by now:
We just need to remove the user_id and the total params as the user
id is not really necessary and the total is being calculated through the model.
After making the changes the code should look like:
If you run the tests now, they will pass, but first, let’s remove the total and
user_id from the permitted params and avoid the mass-assignment.
The order_params method should look like this:
def order_params
params.require(:order).permit(:product_ids => [])
end
We will add the products association and the total attribute to the order
output, and to make sure everything is running smooth, we will some specs.
In order to avoid duplication on tests, I’ll just add one spec for the show and
make sure the extra data is being rendered, this is because I’m using the same
serializer everytime an order object is being parsed to json, so in this case I
would say it is just fine:
By now we should have failing tests. But they are easy to fix on the order
serializer
If you recall Chapter 7 on Box 7.2 we embeded the user into the product,
in order to retrieve some information, but in this we always know the user,
because is actually the current_user so there is no point on adding it, it is
not efficient, so let’s fix that by adding a new serializer:
We want to keep the products information consistent with the one we cur-
rently have, so we can just inherit behavior from it like so:
This will keep rendered data on sync, and now to remove the embebed user
we simply add the following method on the gem documentation. For more
information visit ActiveModelSerializer:
$ git commit -m "Adds a custom order product serializer to remove the user association"
[chapter8 ecac23c] Adds a custom order product serializer to remove the user association
3 files changed, 20 insertions(+), 2 deletions(-)
create mode 100644 app/serializers/order_product_serializer.rb
To make it easy to test the email, we will use a gem called email_spec, it
includes a bunch of useful matchers for mailers, which makes it easy and fun.
So first let’s add the gem to the Gemfile
160 CHAPTER 8. PLACING ORDERS
#line 37
group :test do
gem "rspec-rails", "~> 2.14"
gem "shoulda-matchers"
gem "email_spec"
end
Now run the bundle install command to install all the dependencies.
I’ll follow the documentation steps to setup the gem, you can do so on
https://github.com/bmabey/email-spec#rspec.
When you are done, your spec_helper.rb file should look like:
.
.
.
require "email_spec"
.
.
.
RSpec.configure do |config|
.
.
.
config.include(EmailSpec::Helpers)
config.include(EmailSpec::Matchers)
.
.
.
end
Now we can add some tests for the order mailer we created earlier:
describe OrderMailer do
include Rails.application.routes.url_helpers
describe ".send_confirmation" do
before(:all) do
@order = FactoryGirl.create :order
@user = @order.user
@order_mailer = OrderMailer.send_confirmation(@order)
8.5. SEND ORDER CONFIRMATION EMAIL 161
end
it "should be set to be delivered to the user from the order passed in" do
@order_mailer.should deliver_to(@user.email)
end
I simply copied and pasted the one from the documentation and adapt it to
our needs. We now have to make sure this tests pass.
First we add the action on the order mailer:
def send_confirmation(order)
@order = order
@user = @order.user
mail to: @user.email, subject: "Order Confirmation"
end
end
After adding this code, we now have to add the corresponding views. It is
a good practice to include a text version along with the html one.
162 CHAPTER 8. PLACING ORDERS
$ touch app/views/order_mailer/send_confirmation.html.erb
$ touch app/views/order_mailer/send_confirmation.txt.erb
<ul>
<% @order.products.each do |product| %>
<li><%= product.title %> - <%= number_to_currency product.price %></li>
<% end %>
</ul>
We just need to call the send_confirmation method into the create ac-
tion on the orders controller:
8.6. CONCLUSION 163
def create
order = current_user.orders.build(order_params)
if order.save
OrderMailer.send_confirmation(order).deliver
render json: order, status: 201, location: [:api, current_user, order]
else
render json: { errors: order.errors }, status: 422
end
end
To make sure we did not break anything on the orders, we can just run the
specs from the orders controller:
8.6 Conclusion
Hey you made it!, give yourself an applause, I know it’s been a long way now,
but you are almost done, believe me!.
On chapters to come we will keep working on the Order model to add
some validations when placing an order, some scenarios are:
164 CHAPTER 8. PLACING ORDERS
Next chapter will be short but is really important for the sanity of the app,
so don’t skip it.
Show me some love on twitter:
I just finished chapter 8 of Api on Rails tutorial by @kurenn!
After chapter 9, we will focus on optimization, pagination and some other
cool stuff that will definitely help you build a better app.
Chapter 9
Improving orders
Back in Chapter 8 we extended our API to place orders and send a confirmation
email to the user (just to improve the user experience). This chapter will take
care of some validations on the order model, just to make sure it is placeable,
just like:
We’ll probably need to update a little bit the json output for the orders,
but let’s not spoil things up.
So now that we have everything clear, we can get our hands dirty. You can
clone the project up to this point with:
165
166 CHAPTER 9. IMPROVING ORDERS
Wait, don’t run the migrations just yet, we are making a small modification
to it. As a good practice I like to add default values for the database just to
make sure I don’t mess things up with null values. This is a perfect case!
Your migration file should look like this:
Now it is time to decrement the quantity for the product once an order
is placed. Probably the first thing that comes to your mind is to take this to
the Order model and this is a common mistake when working with Many-to-
Many associations, we totally forget about the joining model which in this case
is Placement.
The Placement is a better place to handle this as we have access to the
order and the product, so we can easily in this case decrement the product
stock.
Before we start implementing the code for the decrement, we have to change
the way we handle the order creation as we now have to accept a quantity for
9.1. DECREMENTING THE PRODUCT QUANTITY 167
each product. If you recall Listing 8.9 we are expecting an array of product
ids. I’m going to try keep things simple and will send a array of arrays where
the first position of each inner array will be the product id and the second the
quantity.
A quick example on this would be something like:
# The first position for the inner arrays is the product id and
# the second the quantity to buy
product_ids_and_quantities = [[1,4], [3,5]]
This is going to be tricky so stay with me, let’s first build some unit tests:
require 'spec_helper'
describe Order do
.
.
.
describe '#set_total!' do
...
end
describe "#build_placements_with_product_ids_and_quantities" do
before(:each) do
product_1 = FactoryGirl.create :product, price: 100, quantity: 5
product_2 = FactoryGirl.create :product, price: 85, quantity: 10
end
def set_total!
...
end
def build_placements_with_product_ids_and_quantities(product_ids_and_quantities)
product_ids_and_quantities.each do |product_id_and_quantity|
id, quantity = product_id_and_quantity # [1,5]
self.placements.build(product_id: id)
end
end
end
And if we run our tests, they should be all nice and green:
require 'spec_helper'
describe Api::V1::OrdersController do
.
.
.
describe "POST #create" do
before(:each) do
current_user = FactoryGirl.create :user
api_authorization_header current_user.auth_token
end
if order.save
order.reload #we reload the object so the response displays the product objects
OrderMailer.send_confirmation(order).deliver
render json: order, status: 201, location: [:api, current_user, order]
else
render json: { errors: order.errors }, status: 422
end
end
end
FactoryGirl.define do
factory :product do
title { FFaker::Product.product_name }
price { rand() * 100 }
published false
user
quantity 5 #this is the line we added
end
end
$ git add .
$ git commit -m "Allows the order to be placed along with product quantity"
Did you notice we are not saving the quantity for each product anywhere?,
there is no way to keep track of that. This can be fix really easy, by just adding
a quantity attribute to the Placement model, so this way for each product we
save its corresponding quantity. Let’s start by creating the migration:
Let’s document the quantity attribute through a unit test like so:
require 'spec_helper'
describe Placement do
.
.
.
it { should respond_to :product_id }
it { should respond_to :quantity }
.
.
.
end
product_ids_and_quantities.each do |product_id_and_quantity|
id, quantity = product_id_and_quantity # [1,5]
$ git add .
$ git commit -m "Adds quantity to placements"
end
9.1. DECREMENTING THE PRODUCT QUANTITY 173
describe Placement do
.
.
.
it { should respond_to :quantity }
.
.
.
describe "#decrement_product_quantity!" do
it "decreases the product quantity by the placement quantity" do
product = placement.product
expect{placement.decrement_product_quantity!}.to change{product.quantity}.by(-placement.qu
end
end
end
And now we just have to hook this method into an after_create callback
and we should be good to go:
.
after_create :decrement_product_quantity!
def decrement_product_quantity!
self.product.decrement!(:quantity, quantity)
end
end
We just now need to make sure that there are enough products on stock
to create a placement record, but first it would be a good idea to commit the
changes:
$ git add .
$ git commit -m "Decrements the product quantity by the placement quantity"
$ mkdir app/validators
$ touch app/validators/enough_products_validator.rb
Before we drop any line of code, we need to make sure to add a spec to the
Order model to check if the order can be placed.
describe Order do
.
.
.
describe "#build_placements_with_product_ids_and_quantities" do
end
describe "#valid?" do
before do
product_1 = FactoryGirl.create :product, price: 100, quantity: 5
product_2 = FactoryGirl.create :product, price: 85, quantity: 10
As you can see on the spec, we first make sure that placement_2 is trying
to request more products than are available, so in this case the order is not
supposed to be valid.
The test by now should be failing, let’s turn it into green by adding the code
for the validator:
176 CHAPTER 9. IMPROVING ORDERS
I manage to add a message for each of the products that are out of stock, but
you can handle it differently if you want. Now we just need to add the validator
to the Order model like so:
And now if you run your tests, everything should be nice and green:
$ git add .
$ git commit -m "Adds validator for order with not enough products on stock"
[chapter9 7bd9db4] Adds validator for order with not enough products on stock
3 files changed, 31 insertions(+)
create mode 100644 app/validators/enough_products_validator.rb
9.3. UPDATING THE TOTAL 177
def set_total!
self.total = products.map(&:price).sum
end
Now instead of calculating the total by just adding the product prices, we
need to multiply it by the quantity, so let’s update the spec first:
describe '#set_total!' do
before(:each) do
product_1 = FactoryGirl.create :product, price: 100
product_2 = FactoryGirl.create :product, price: 85
def set_total!
self.total = 0
placements.each do |placement|
self.total += placement.product.price * placement.quantity
end
end
178 CHAPTER 9. IMPROVING ORDERS
9.4 Conclusion
Oh you are here!, let me congratulate you, it’s been a long way since chapter
1, but you are 1 step closer. Actually the next chapter would be the last one, so
try to take the most out of it.
The last chapter would be on how to optimize the API by using pagination,
caching and background jobs, so buckle up, it is going to be a bumpy ride.
Show me some love on twitter:
I just finished chapter 9 of Api on Rails tutorial by @kurenn!
Chapter 10
Optimization
Welcome to the last chapter of the book, it’s been a long way and you are only
one step away from the end. Back in Chapter 9 we finish modeling the Order
model and we could say that the project is done by now, but I want to cover
some important details about optimization. The topics I’m going to cover in
here will be:
• Pagination
• Background Jobs
• Caching
179
180 CHAPTER 10. OPTIMIZATION
$ cd market_place_api/
$ git checkout -b chapter10
Switched to a new branch 'chapter10'
10.1 Pagination
A very common strategy to optimize an array of records from the database, is
to load just a few by paginating them and if you are familiar with this tech-
nique you know that in Rails is really easy to achieve it wheter if you are using
will_paginate or kaminari.
Then only tricky part in here is how are we suppose to handle the json
output now, to give enough information to the client on how the array is pag-
inated. If you recall Chapter 1 I shared some resources on the practices I was
going to be following in here, one of them was http://jsonapi.org/ which is a
must-bookmark page.
If we read the format section we will reach a sub section called Top Level
and in very few words they mention something about pagination:
“meta”: meta-information about a resource, such as pagination.
It is not very descriptive but at least we have a hint on what to look next
about the pagination implementation, but don’t worry that is exactly what we
are going to do in here.
Let’s start with the products list.
10.1.1 Products
We are going to start nice and easy by paginating the products list as we don’t
have any kind of access restriction which leads to easier testing.
First we need to add the kaminari gem to our Gemfile:
gem 'kaminari'
$ bundle install
def index
respond_with Product.search(params).page(params[:page]).per(params[:per_page])
end
So far the only thing that changed is the query on the database to just limit
the result by 25 per page which is the default, but we have not added any extra
information to the json output.
We need to provide the pagination information on the meta tag in the fol-
lowing form:
"meta": {
"pagination": {
"per_page": 25,
"total_page": 6,
"total_objects": 11
}
}
Now that we have the final structure for the meta tag we just need to output
it on the json response, let’s first add some specs:
describe Api::V1::ProductsController do
before(:each) do
4.times { FactoryGirl.create :product }
end
We should have 6 failing tests by now, yes you read it right 6, although we
just added 5, which means we broke something else.
10.1. PAGINATION 183
If you are wondering which test I’m talking about here it is:
1) Api::V1::ProductsController GET #index when product_ids parameter is sent returns just the pr
Failure/Error: get :index, product_ids: @user.product_ids
NoMethodError:
undefined method `page' for #<Array:0x007fabb761a818>
# ./app/controllers/api/v1/products_controller.rb:8:in `index'
# ./spec/controllers/api/v1/products_controller_spec.rb:59:in `block (4 levels) in <top (re
The error is actually on the search method of the Product model, be-
cause for some reason kaminari is expecting an active record relation instead
of an array. It is actually really easy to fix:
app/models/product.rb
Did you notice the change?, if not let me tell you what changed. Instead of
fetching the record using the find method with the product_ids params, I just
change it to a where clause which returns an ActiveRecord::Relation, exactly
what we need.
Now if we run the specs again, that spec should be green:
Now that we fixed that, let’s add the pagination information, we need to do
it on the products_controller.rb file:
def index
products = Product.search(params).page(params[:page]).per(params[:per_page])
render json: products, meta: { pagination:
{ per_page: params[:per_page],
total_pages: products.total_pages,
total_objects: products.total_count } }
end
.
.
.
end
Now we have make a really amazing optimization for the products list
endpoint, now it is the client job to fetch the correct page with the correct
per_page param for the records.
Let’s commit this changes and proceed with the orders list.
$ git commit -m "Adds pagination for the products index action to optimize response"
[chapter10 6716515] Adds pagination for the products index action to optimize response
5 files changed, 18 insertions(+), 4 deletions(-)
10.1. PAGINATION 185
10.1.2 Orders
Now it is time to do exactly the same for the orders list endpoint, which by
now should be really easy to implement. But first, let’s add some specs to the
orders_controller_spec.rb file:
require 'spec_helper'
describe Api::V1::OrdersController do
def index
orders = current_user.orders.page(params[:page]).per(params[:per_page])
render json: orders, meta: { pagination:
{ per_page: params[:per_page],
total_pages: orders.total_pages,
total_objects: orders.total_count } }
end
.
.
.
end
it { expect(json_response).to have_key(:meta) }
it { expect(json_response[:meta]).to have_key(:pagination) }
it { expect(json_response[:meta][:pagination]).to have_key(:per_page) }
it { expect(json_response[:meta][:pagination]).to have_key(:total_pages) }
it { expect(json_response[:meta][:pagination]).to have_key(:total_objects) }
$ mkdir spec/support/shared_examples
$ touch spec/support/shared_examples/pagination.rb
And on the pagination.rb file you can just add the following lines:
This shared example can now be use as a substitude for the 5 tests on the
orders_controller_spec.rb and products_controller_spec.rb files
like so:
spec/controllers/api/v1/orders_controller_spec.rb
spec/controllers/api/v1/products_controller_spec.rb
188 CHAPTER 10. OPTIMIZATION
include Authenticable
protected
And now we can substitude the pagination hash on both controllers for the
method, like so:
app/controllers/api/v1/orders_controller.rb
def index
orders = current_user.orders.page(params[:page]).per(params[:per_page])
render json: orders, meta: pagination(orders, params[:per_page])
end
app/controllers/api/v1/products_controller.rb
10.2. BACKGROUND JOBS 189
def index
products = Product.search(params).page(params[:page]).per(params[:per_page])
render json: products, meta: pagination(products, params[:per_page])
end
If you run the specs for each file they should be all nice and green:
This would be a good moment to commit the changes and move on to the
next Section 10.2 about background jobs.
thing that could steal some CPU time is sending mails, which in our case, we
are just sending a confirmation for the order.
Although it won’t take much I find this relevant for future long-time pro-
cesses and allow the server not to lock. There are so many options out there for
handling this, Sidekiq, Resque, or Delayed Job, we will be using the last one,
just to keep things simple.
We first add the gem to the Gemfile:
gem 'delayed_job_active_record'
And then run the bundle command to install it along with the delayed_job
generate method:
$ bundle install
$ rails generate delayed_job:active_record
$ rake db:migrate
This will generate a table for delayed jobs where are going to be enque for
later processing. For more information head to the documentation
And now that we have eveything setup, we just need to update the line
where we send the email after the order has been placed, like so:
app/controllers/api/v1/orders_controller.rb
.
.
.
def create
order = current_user.orders.build
order.build_placements_with_product_ids_and_quantities(params[:order][:product_ids_and_quant
if order.save
order.reload #we reload the object so the response displays the product objects
OrderMailer.delay.send_confirmation(order) #this is the line
render json: order, status: 201, location: [:api, current_user, order]
else
render json: { errors: order.errors }, status: 422
end
end
.
.
.
10.3. API CACHING 191
MarketPlaceApi::Application.configure do
.
.
.
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
def cache_key
[object, scope]
end
end
As you can see the implementation is fairly easy and the benefits are huge,
but be aware for any further change on the caching implementation that gem
may implement. I’ll add the cached and cache_key methods on every seri-
alizer just for demonstration purposes.
On a real environment you probably want to handle this with care.
Let’s commit the changes and wrap up:
10.4. CONCLUSION 193
10.4 Conclusion
If you reach this point that means you are done with the book, good job!, you
just become a great API Rails developer that’s for sure.
Thanks for taking this great adventure all along with me, I hope you enjoyed
the travel as much as I did. We should take a beer sometime.
Show me some love on twitter:
I’m a Rails API rockstar thanks to @kurenn!