Api Rails ¨6

Table of contents :
API on Rails 6
Table of Contents
Before
Foreword
About the author
Copyright and license
Thanks
Introduction
Conventions on this book
Development environments
Text editors and Terminal
Browsers
Package manager
Git
Ruby
Initializing the project
Versioning
Conclusion
The API
Planning the application
Setting the API
Routes, Constraints and Namespaces
Api versioning
Conclusion
Presenting users
User model
Generation of the User model
Password hash
Build users
Test our resource with cURL
Create users
Update users
Delete the user
Conclusion
Authenticating user
Stateless session
JWT presentation
Setting up the authentication token
Token’s controller
Logged user
Authentication with the token
Authorize actions
Conclusion
User’s products
The product model
The foundations of the product
Product validations
Products endpoints
Show action for products
Products list
Creating products
Updating products
Destroying products
Feed the database
Conclusion
Building JSON
Presentation of JSON:API
Serialize user
Serialize products
Serialize associations
Theory of the injection of relationships
Integrate into a meta attribute
Incorporate the object into the attribute
Incorporate the relationships into `include
Application of the injection of relationships
Retrieve user’s products
Search for products
The keyword by
By price
Sort by creation date
Conclusion
Placing Orders
Modeling order
Orders and Products
Expose the user model
Render a single order
Placing and order
Send order confirmation email
Conclusion
Improving orders
Decrementing product quantity
Extending the Placement model
Validate quantity of products
Updating the total
Conclusion
Optimizations
Pagination
Products
Orders list
Refactoring pagination
API Caching
N+1 Queries
Prevention of N + 1 requests
Activation of CORS
Conclusion

Citation preview

{

}

"APIonRails":6

Alexandre

Rousseau

{

}

"APIonRails":6

API on Rails 6 Alexandre Rousseau Version 6.0.5, 2020-01-09

Table of Contents Before . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1  

Foreword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 About the author . . . . . . . . . . . . . . . . . . . . . . . . . . 3  

 

Copyright and license . . . . . . . . . . . . . . . . . . . . . . . 4 Thanks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5  

 

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Conventions on this book . . . . . . . . . . . . . . . . . . . . . . 8  

 

Development environments . . . . . . . . . . . . . . . . . . . . . 9 Text editors and Terminal . . . . . . . . . . . . . . . . . . . . 9  

 

Browsers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Package manager. . . . . . . . . . . . . . . . . . . . . . . . . 10  

 

Git . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Ruby . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10  

 

Initializing the project . . . . . . Versioning . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . The API . . . . . . . . . . . . . . . . Planning the application . . . . . . Setting the API . . . . . . . . . . . Routes, Constraints and Namespaces Api versioning . . . . . . . . . . . Conclusion . . . . . . . . . . . . . Presenting users . . . . . . . . . . . User model . . . . . . . . . . . . . Generation of the User model . . . Password hash. . . . . . . . . . . Build users . . . . . . . . . . . . . Test our resource with cURL. . . . Create users . . . . . . . . . . . Update users . . . . . . . . . . . Delete the user . . . . . . . . . . Conclusion . . . . . . . . . . . . . Authenticating user . . . . . . . . . . Stateless session . . . . . . . . . . JWT presentation . . . . . . . . . Setting up the authentication token Token’s controller . . . . . . . . Logged user . . . . . . . . . . . . . Authentication with the token . . . Authorize actions . . . . . . . . . Conclusion . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

13 14 16 17 18 19 20 23 25 26 27 27 32 35 38 39 41 43 46 47 48 48 49 51 55 59 59 62

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

User’s products . . . . . . . . . . . . . . . . . . . . . . . . . . . 63  

The product model . . . . . . . . . . . . . . . . . . . . . . . . 64 The foundations of the product . . . . . . . . . . . . . . . . . 64  

 

Product validations . . . . . . . . . . . . . . . . . . . . . . . 67 Products endpoints . . . . . . . . . . . . . . . . . . . . . . . . 69  

 

Show action for products . . . . . . . . . . . . . . . . . . . . 69 Products list . . . . . . . . . . . . . . . . . . . . . . . . . . 71  

 

Creating products . . . . . . . . . . . . . . . . . . . . . . . . 72 Updating products . . . . . . . . . . . . . . . . . . . . . . . . 75  

 

Destroying products . . . . . . . . . . . . . . . . . . . . . . . 78 Feed the database . . . . . . . . . . . . . . . . . . . . . . . . . 81  

 

Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 Building JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85  

 

Presentation of JSON:API . . . . . . . . . . . . . . . . . . . . . 86 Serialize user . . . . . . . . . . . . . . . . . . . . . . . . . . 87  

 

Serialize products . . . . . . . . . . . . . . . . . . . . . . . . 90 Serialize associations . . . . . . . . . . . . . . . . . . . . . 92 Theory of the injection of relationships . . . . . . . . . . . . . 94 Integrate into a meta attribute. . . . . . . . . . . . . . . . . 94 Incorporate the object into the attribute . . . . . . . . . . . . 94 Incorporate the relationships into `include . . . . . . . . . . . 97 Application of the injection of relationships . . . . . . . . . . . 99 Retrieve user’s products . . . . . . . . . . . . . . . . . . . . 102 Search for products . . . . . . . . . . . . . . . . . . . . . . . 106 The keyword by. . . . . . . . . . . . . . . . . . . . . . . . . 106 By price . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 Sort by creation date . . . . . . . . . . . . . . . . . . . . . 110 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 Placing Orders . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 Modeling order . . . . . . . . . . . . . . . . . . . . . . . . . 116 Orders and Products . . . . . . . . . . . . . . . . . . . . . . 117 Expose the user model . . . . . . . . . . . . . . . . . . . . . . 119 Render a single order . . . . . . . . . . . . . . . . . . . . . 121 Placing and order . . . . . . . . . . . . . . . . . . . . . . . 123  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Send order confirmation email . . . . . . . . . . . . . . . . . . 130 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133  

 

Improving orders . . . . . . . . . . . . . . . . . . . . . . . . . 134 Decrementing product quantity . . . . . . . . . . . . . . . . . . 135  

 

Extending the Placement model . . . . . . . . . . . . . . . . . 141 Validate quantity of products . . . . . . . . . . . . . . . . . . 143  

 

Updating the total . Conclusion . . . . . Optimizations . . . . Pagination . . . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

145 147 148 149

 

 

 

 

Products . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149  

Orders list . . . . . . . . . . . . . . . . . . . . . . . . . . 153 Refactoring pagination . . . . . . . . . . . . . . . . . . . . . 155  

 

API Caching . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 N+1 Queries . . . . . . . . . . . . . . . . . . . . . . . . . . . 161  

 

Prevention of N + 1 requests . . . . . . . . . . . . . . . . . . 162 Activation of CORS . . . . . . . . . . . . . . . . . . . . . . . . 166  

 

Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169  

Before

1

Foreword "API on Rails 6" is based on "APIs on Rails: Building REST APIs with Rails". It was initially published in 2014 by Abraham Kuri under the licenses MIT and Beerware. The first version was not maintained and was initially planned for Ruby on Rails version 4 which does not receives more than security updates. I wanted to update this excellent book, adapting it to new versions of Ruby on Rails. This book is therefore available for Ruby on Rails versions 5.2 and 6.0 (the one you are currently reading).

NOTE

2

This book is also available in the Molière language (It means french).

About the author My name is Alexandre Rousseau and I am a Rails developer with more than 4 years of experience (at the time of writing). I am currently a partner in a company (iSignif) where I build and maintain a SAAS product using Rails. I also contribute to the Ruby community by producing and maintaining some gems that you can consult on my Rubygems.org profile. Most of my projects are on GitHub so don’t hesitate to follow me. All the source code of this book is available in Asciidoctor format on GitHub. So feel free to fork the project if you want to improve it or fix mistakes I didn’t notice.

3

Copyright and license This book is provided on MIT license. All the book’s source code is available on Markdown format on GitHub

MIT license Copyright 2019 Alexandre Rousseau Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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.

"API on Rails 6" by Alexandre Rousseau is shared according to Creative Commons Attribution - Attribution-ShareAlike 4.0 International. Built upon this book http://apionrails.icalialabs.com/book/. This book’s cover picture uses a beautiful photo shot by Yoann Siloine who published it on Unsplash.

4

Thanks A big "thank you" to all Github contributors who keep this book alive. In alphabetical order: • airdry • Landris18 • lex111 • cuilei5205189 • franklinjosmell • notapatch • tacataca

5

Introduction Welcome to API on Rails 6, a tutorial on steroid to learn the best way to build your next API with Rails. The purpose of this book is to provide a comprehensive methodology to develop a RESTful API following best practices. As soon as you finish this book, you will be able to create your own API and integrate it with any client such as a web browser or mobile application. The generated code is built with Ruby on Rails 6.0 which is the current version. The purpose of this book is not only to teach you how to build an API with Rails but rather to teach you how to build an evolutive and maintainable API with Rails. That is, improve your current knowledge with Rails. On this journey, you will learn to: • Use Git for version control • Building JSON responses • Test your end-points with unit and functional tests • Set up authentication with JSON Web Tokens (JWT) • Use JSON:API specification • Optimize and cache the API I strongly recommend you follow all the steps in this book. Try not to skip chapters because I will give you some tips and tricks to improve your skills throughout the book. You can consider yourself the main character of a video game who gain a level in each chapter. In this first chapter I will explain how to configure your environment (in case you don’t already have it). Then we will create an application called market_place_api. I will ensure that I teach you the best practices I have learned during my experience. This means that we’ll start using Git just after initializing the project. We’ll build the application following a simple working method that I use daily in the next chapters. We will develop the entire application using Test Driven Development (TDD). I will also explain the interest of using an API for your next project and choosing a suitable response format such as JSON or XML. Further on, we will get our hands on the code and complete the basics of the application by building all the necessary roads. We will also secure access to the API by building authentication by exchanging HTTP headers. Finally, in the last chapter, we will add some optimization techniques to improve the

6

structure and response times of the server. 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 Magento.

7

Conventions on this book The conventions on this book are based on the ones from Ruby on Rails Tutorial. In this section I’ll mention some that may not be so clear. I’ll be using many examples using command-line commands. I won’t deal with windows cmd (sorry guys), so I’ll based all the examples using Unix-style command line prompt, as follows:

$ echo "A command-line command" A command-line command I’ll be using some guidelines related to the language, what I mean by this is: • Avoid means you are not supposed to do it • Prefer indicates that from the 2 options, the first it’s a better fit • Use means you are good to use the resource 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 email me.

8

Development environments One of the most painful parts for almost every developer is setting everything up, but as long as you get it done, the next steps should be a piece of cake and well rewarded. So I will guide you to keep you motivated.

Text editors and Terminal There are many cases in which development environments may differ from computer to computer. That is not the case with text editors or IDE’s. I think 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 come with many integrations out of the box. • 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 crossplatform 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 using 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 recommend. 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 terminal 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 Firefox immediately, but some other developers may say Chrome 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.

9

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 I install software and I manage it. To install brew just run the command below:

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/inst all)" • 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 install 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. • on Mac OS: $ brew install git • on Linux: $ sudo apt-get install git

Ruby There are many ways in which you can install and manage ruby, and by now you should probably have some version installed if you are on Mac OS, to see which version you have, just type:

$ ruby -v Rails 6.0 requires the installation of version 2.5 or higher. I recommend using Ruby Version Manager (RVM) or rbenv to install it. We will use RVM in this tutorial but it doesn’t matter which of these two options you use The principle of these tools is allowing you to install several versions of Ruby on the same machine, in an environment that is airtight to a possible version installed on your operating system and to be able to switch from one to the other easily.

10

To install RVM, go to https://rvm.io/ and install the GPG footnote key:[The GPG key allows you to verify the identity of the author of the sources you download.]. Once that’s done:

$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB $ \curl -sSL https://get.rvm.io | bash Next it is time to install ruby:

$ rvm install 2.6 Now it is time to install the rest of the dependencies we will be using.

Gems, Rails & Missing libraries First we update the gems on the whole system:

$ gem update --system On some cases if you are on a Mac OS, you will need to install some extra libraries:

$ brew install libtool libxslt libksba openssl We then install the necessary gems and ignore documentation for each gem:

$ gem install bundler $ gem install rails -v 6.0.0 Check for everything to be running nice and smooth:

$ rails -v Rails 6.0.0

11

Database I highly recommend you install Postgresql to manage your databases. But here we’ll be using SQlite for simplicity . 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:

$ sudo apt-get install libxslt-dev libxml2-dev libsqlite3-dev or

$ sudo yum install libxslt-devel libxml2-devel libsqlite3-devel

12

Initializing the project Initializing a Rails application may be pretty straightforward for you. If that is not the case here is a super quick tutorial. There is the command:

$ mkdir ~/workspace $ cd ~/workspace $ rails new market_place_api --api

NOTE

The --api option appeared in version 5 of Rails. It allows you to limit the libraries and Middleware included in the application. This also avoids generating HTML views when using Rails generators.

As you may guess, the commands above will generate the bare bones of your Rails application.

13

Versioning Remember that Git helps you track and maintain your code history. Keep in mind that the source code of the application is published on GitHub. You can follow the project on GitHub. Ruby on Rails initialized the Git directory for you when you used the rails new command. This means that you do not need to execute the git init command. However it is necessary to configure the information of the author of commits. If you have not already done so, go to the directory and run the following commands:

$ git config --global user.name "Type in your name" $ git config --global user.email "Type in your email" Rails also provides a .gitignore file to ignore some files that we don’t want to track. The default .gitignore file should look like the one shown below: .gitignore

# Ignore bundler config. /.bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore uploaded files in development. /storage/* !/storage/.keep .byebug_history # Ignore master key for decrypting credentials and more. /config/master.key

14

After modifying the .gitignore file we just need to add the files and commit the changes, the necessary commands are shown below:

$ git add . $ git commit -m "Initial commit"

TIP

I have found that committing a message starting with a present tense verb, describing what the commit does and not what it did, helps when you are exploring the history of the project. I find it is more natural to read and understand. I’ll follow this practice until the end of the tutorial.

Lastly and as an optional step we setup the GitHub (I’m not going through that in here) project and push our code to the remote server: We first add the remote:

$ git remote add origin [email protected]:madeindjs/market_place_api_6.git Then we push the code:

$ git push -u origin master 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.

15

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. So let’s get our hands dirty and start typing some code!

16

The API In this section I’ll outline the application. By now you should have read the previous chapter. If you did not read it I recommend you to do it. You can clone the project until this point with:

$ git checkout tags/checkpoint_chapter02 To summarize, we simply generated our Rails application and made our first commit.

17

Planning the application As we want to go simple with the application it consists on five models. Don’t worry if you don’t fully understand what is going on. We will review and build each of these resources as we move on with the tutorial.

In brief, the user will be able to place many orders, upload multiple products which can have many images or comments from another users on the app. We are not going to build views for displaying or interacting with the API, so not to make this a huge tutorial, I’ll let that to you. There are plenty of options out there like javascript frameworks (Angular, Vue.js, React.js). By this point you must be asking yourself: all right but I need to explore or visualize the api we are going to be building? That’s fair. Probably if you google something related to api exploring, an application called Postman will pop. It is a great software but we won’t be using that anyway because we’ll use cURL who allow anybody to reproduce request on any computer.

18

Setting the API An API is defined by wikipedia as an application programming interface (API) specifies how some software components should interact with each other. In other words the way systems interact with each other through a common interface, in our case a web service built with JSON. There are other kinds of communication protocols like SOAP, but we are not covering that in here. JSON, as the Internet media type standard, is widely accepted, readable, extensible and easy to implement. Many of the current frameworks consume JSON API’s by default (Angular or Vue.js for example). There are also great libraries for Objective-C too like AFNetworking or RESTKit. There are probably good solutions for Android but because of my lack of experience on that development platform I might not be the right person to recommend you something. All right. So we are building our API with JSON. There are many ways to achieve this. The first thing that comes to mind would be just to start adding routes defining the end points. This may be bad because they may not have a URI pattern clear enough to know which resource is being exposed. The protocol or structure I’m talking about is REST which stands for Representational State Transfer and by wikipedia definition

aService.getUser("1") And in REST you may call a URL with an specific HTTP request, in this case with a GET request: http://domain.com/resources_name/uri_pattern RESTful APIs must follow at least three simple guidelines: • A base URI, such as http://example.com/resources/. • An Internet media type to represent the data, it is commonly JSON and is commonly set through headers exchange. • Follows the standard HTTP Methods such as GET, POST, PUT, DELETE. ◦ GET: Reads the resource or resources defined by the URI pattern ◦ POST: Creates a new entry into the resources collection ◦ PUT: Updates a collection or member of the resources ◦ DELETE: Destroys a collection or member of the resources 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

19

lot easier to understand.

Routes, Constraints and Namespaces Before start typing any code, we prepare the code with git. We’ll be using a branch per chapter, upload it to GitHub and then merge it with master. So let’s get started open the terminal, cd to the market_place_api directory and type in the following:

$ git checkout -b chapter02 Switched to a new branch 'chapter02' We are only going to be working on the config/routes.rb, as we are just going to set the constraints and the default response format for each request. config/routes.rb

Rails.application.routes.draw do   # ... end 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:

$ git add config/routes.rb $ git commit -m "Removes comments from the routes file" We are going to isolate the api controllers under a namespace. With Rails this is fairly simple: you just have to create a folder under the app/controllers named api. The name is important because that’s the namespace we’ll use for managing the controllers for the api endpoints.

$ mkdir app/controllers/api Then we add that namespace into our routes.rb file:

20

config/routes.rb

Rails.application.routes.draw do   # Api definition   namespace :api do   # We are going to list our resources here   end end By defining a namespace under the routes.rb file. Rails will automatically map that namespace to a directory matching the name under the controllers folder, in our case the api/` directory.

Rails media types supported Rails can handle up to 35 different media types, you can list them by accessing the SET class under de Mime module:

$ rails c 2.6.3 :001 > Mime::SET.collect(&:to_s)  => ["text/html", "text/plain", "text/javascript", "text/css", "text/calendar", "text/csv", "text/vcard", "text/vtt", "image/png", "image/jpeg", "image/gif", "image/bmp", "image/tiff", "image/svg+xml", "video/mpeg", "audio/mpeg", "audio/ogg", "audio/aac", "video/webm", "video/mp4", "font/otf", "font/ttf", "font/woff", "font/woff2", "application/xml", "application/rss+xml", "application/atom+xml", "application/x-yaml", "multipart/form-data", "application/x-www-formurlencoded", "application/json", "application/pdf", "application/zip", "application/gzip"]

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:

21

config/routes.rb

Rails.application.routes.draw do   # Api definition   namespace :api, defaults: { format: :json }   # We are going to list our resources here   end end

do

Up to this point we have not made anything crazy. What we want to generate is a base_uri wich include the API version. But let’s make a comit before go to next section:

$ git add config/routes.rb $ git commit -m "Set the routes constraints for the api"

22

Api versioning At this point we should have a nice routes mapping using a namespace. Your routes.rb file should look like this: config/routes.rb

Rails.application.routes.draw do   # Api definition   namespace :api, defaults: { format: :json }   # We are going to list our resources here   end end

do

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:

$ mkdir app/controllers/api/v1 This way we can namespace our api into different versions very easily, now we just need to add the necessary code to the routes.rb file: config/routes.rb

Rails.application.routes.draw do   # Api definition   namespace :api, defaults: { format: :json } do   namespace :v1 do   # We are going to list our resources here   end   end end By this point the API is now scoped via the URL. For example with the current configuration an end point for retrieving a product would be

23

like: http://localhost:3000/v1/products/1 .

Common API patterns You can find many approaches to set up the base_uri when building an api following different patterns, assuming we are versioning our api: • api.example.com/: In my opinion this is the way to go, gives you a better interface and isolation, and in the long term can help you to quickly scalate • example.com/api/: This pattern is very common, and it is actually a good way to go when you don’t want to namespace your api under a subdomain • example.com/api/v1: it seems like a good idea, by setting the version of the api through the URL seems like a more descriptive pattern, but this way you enforce the version to be included on URL on each request, so if you ever decide to change this pattern, this becomes a problem of maintenance in the long-term There are some practices in API building that recommend not to version the API via the URL. That’s true. The developer should not be aware of the version he’s using. For the sake of simplicity, I have chosen to set aside this convention, which we will be able to apply in a second phase.

It is time to commit:

$ git commit -am "Set the versioning namespaces for API" We are at the end of our chapter. It is therefore time to apply all our modifications to the master branch by making a merge. To do this, we place ourselves on the master branch and we merge chapter02:

$ git checkout master $ git merge chapter02

24

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 if you feel curious there are some gems that handle this kind of configuration: • RocketPants • Versionist I’m not covering those in this book, 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.

25

Presenting users In the last chapter we manage to set up the bare bones for our application endpoints configuration. In a next chapter we will handle users authentication through authentication tokens as well as setting permissions to limit access for let’s say signed in users. In coming chapters we will relate products to users and give them the ability to place orders. You can clone the project until this point with:

$ git checkout tags/checkpoint_chapter03 As you can already imagine there are a lot of authentication solutions for Rails, AuthLogic, Clearance and Devise. These solutions are turnkey libraries, i.e. they allow you to manage a whole bunch of things like authentication, password forgetfulness, validation, etc… Nevertheless we will use bcrypt gem to hash the user’s password. This chapter will be complete. It may be a long one but I will try to cover as many topics as possible. Feel free to have a coffee and let’s go. At the end of this chapter you will have built all the user logic as well as the validation and error management. It is a good time to create a new branch:

$ git checkout -b chapter03

NOTE

26

Just make sure you are on the master branch before checking out.

User model Generation of the User model We will start by generating our User model. This model will be really basic and will contain only two fields: • email which will be unique and will allow it to connect to the application • password_digest which will contain the hashed version password (we will discuss this later in this chapter)

of

the

We generate our User model using the generate model command provided by Ruby on Rails. It is very easy to use:

$ rails generate model User email:string password_digest:string invoke active_record   create db/migrate/20190603195146_create_users.rb   create app/models/user.rb   invoke test_unit   create test/models/user_test.rb   create test/fixtures/users.yml

NOTE

The model is the element containing the data as well as the logic related to the data: validation, reading and recording.

This command generates a lot of files! Don’t worry we’ll review them one by one. The migration file contained in the db/migrate folder contains the migration that describes the changes that will be made to the database. This file should look like this:

27

db/migrate/20190603195146_create_users.rb

class CreateUsers < ActiveRecord::Migration[6.0]   def change   create_table :users do |t|   t.string :email   t.string :password_digest   t.timestamps   end   end end

NOTE

The inserted date at the beginning of the migration file name should be different for you since it corresponds to the migration creation date.

We will make a small change to the migration in order to add some database validations. With Rails it is common practice to make these verifications directly in the Ruby model. It is good practice to do so also in the database schema. We will therefore add two additional constraints: • email is mandatory: we use the property null: false. • email must be unique: we add an index for the email column with property unique: true. • password is mandatory: we use the property null: false. The migration thus becomes: db/migrate/20190603195146_create_users.rb

# ... create_table :users do |t|   t.string :email, null: false   t.index :email, unique: true   t.string :password_digest, null: false   # ... end We can run changes once the migration is complete with the following command:

28

db/migrate/20190603195146_create_users.rb

$ rake db:migrate == 20190603195146 CreateUsers: migrating ====================================== -- create_table(:users)   -> 0.0027s == 20190603195146 CreateUsers: migrated (0.0028s) =============================

NOTE

This command will convert our migration into a SQL query that will update the SQlite3 database contained in the db folder.

Model So we defined our database schema. The next step is to update our model to define validation rules. These rules are defined in the template located in the app/models folder. Ruby on Rails provides a complete validation mechanism that can be found at their official documentation. In our case we want to validate only three things: 1. the email must have a valid format 2. the email must be unique 3. the password must be filled in These three rules are defined by the following code: app/models/user.rb

class User < ApplicationRecord   validates :email, uniqueness: true   validates_format_of :email, with: /@/   validates :password_digest, presence: true end There you go. Rails uses a very simple syntax and the code is very readable.

29

Email validation You may notice that the email validation uses a validation by only checking for the presence of a @.

simplistic

That’s normal. There are infinite exceptions to the email address so well that even Look at all these [email protected] is a valid address. It is therefore better to favour a simple approach and confirm the email by a validation email.

Unit tests We end with the unit tests. We use here the Minitest test framework which is provided by default with Rails. Minitest is based on Fixtures which allow you to fill your database with predefined data. Fixtures are defined in YAML files in the tests/fixtures folder. There is one file per template. We must therefore start by updating our tests/fixtures.

NOTE

fixtures are not designed to create all the data your tests need. They just allow you to define the basic data your application needs.

So we will start by creating a fixture defining a user: test/fixtures/users.yml

one:   email: [email protected]   password_digest: hashed_password So we can now create three tests: • 1. Check that a user with valid data is valid:

30

test/models/user_test.rb

# ... test 'user with a valid email should be valid' do   user = User.new(email: '[email protected]', password_digest: 'test')   assert user.valid? end • 2. Check that a user with an invalid email address is not valid: test/models/user_test.rb

# ... test 'user with invalid email should be invalid' do   user = User.new(email: 'test', password_digest: 'test')   assert_not user.valid? end • 3. Check that a new user with a duplicate email is not valid. So we use the same email as the fixture we just created. test/models/user_test.rb

# ... test 'user with taken email should be invalid' do   other_user = users(:one)   user = User.new(email: other_user.email, password_digest: 'test')   assert_not user.valid? end There you go. We can verify that our implementation is correct just by simply running unit tests we have just created:

$ rake test ... 3 runs, 3 assertions, 0 failures, 0 errors, 0 skips I think it’s time to do a little commit to validate our progress:

$ git add . && git commit -m "Create user model"

31

Password hash We have previously implemented the storage of user data. We still have a problem to solve: the storage of passwords is in clear text. If you store user passwords in the clear, then an attacker who steals a copy of your database has a giant list of emails and passwords. Some of your users will only have one password — for their email account, for their banking account, for your application. A simple hack could escalate into massive identity theft. - source - Why you should use bcrypt So we will use the bcrypt gem to hash the password.

NOTE

Hash is the process of transforming a character string into Hash. This Hash does not allow you to find the original character string. However, we can easily use it to find out if a given character string matches the hash we have stored.

We must first add the Bcrypt gem to the Gemfile. We can use the bundle add command. This one will: 1. add the gem to the Gemfile by retrieving the current version 2. launch the command bundle install which will install the gem and update the file Gemfile.lock which "locks" the current version of the gem Therefore, issue the following command:

$ bundle add bcrypt Once the command is executed, the following line is added at the end of the Gemfile: Gemfile

gem "bcrypt", "~> 3.1"

NOTE

Version 3.1 of bcrypt is the current version at the time of writing. It may therefore vary for your case.

Active Record offers us a method ActiveModel::SecurePassword::has_secure_password that will interface

32

with Bcrypt and hack the password for us very easily. app/models/user.rb

class User < ApplicationRecord   # ...   has_secure_password end has_secure_password adds the following validations: • The password must be present when creating. • The password length must be less than or equal to 72 bytes. • The confirmation of the password password_confirmation (if sent)

using

the

attribute

In addition, this method will add a User#password attribute that will be automatically hashed and saved in the User#password_digest attribute. Let’s try this right now in the Rails console. Open a console with rails console:

2.6.3 :001 > User.create! email: '[email protected]', password: '123456'  =># You can see that when you call the User#create! method, the password attribute is hashed and stored in password_digest. We can also send a password_confirmation attribute that ActiveRecord will compare to password:

2.6.3 :002 > User.create! email: '[email protected]', password: '123456', password_confirmation: 'azerty' ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn t match Password) Everything is working as planned! Let’s now make a committe to keep the history concise:

33

$ git commit -am "Setup Bcrypt"

34

Build users It’s time to make our first entry point. We will begin by building the show action which will respond with a single user in the JSON data format. The steps are: 1. generate the users_controller. 2. add the corresponding tests 3. build the real code. Let’s first focus on generating the controller and functional tests. In order to respect the viewing of our API, we will cut application using modules. The syntax is therefore as follows:

our

$ rails generate controller api::v1::users This command will create users_controller_test.rb file. Before going further there are two things we want to test for an API: • The JSON structure returned by the server • The HTTP response code returned by the server

35

Common HTTP codes The first digit of the status code specifies one of the five response classes. The bare minimum for an HTTP client is that it uses one of these five classes. Here is a list of commonly used HTTP codes: • 200: Standard response for successful HTTP requests. This is usually on GET requests • 201: The demand was met and resulted in the creation of a new resource. After the POST requests • 204: The server has successfully processed the request, but does not return any content. This is usually a successful DELETE request. • 400: The request cannot be executed due to bad syntax. Can happen for any type of request. • 401: Similar to 403, but specifically for use when authentication is required and has failed or has not yet been provided. Can happen for any type of request. • 404: The requested resource could not be found but may be available again in the future. Usually concerns GET requests • 500: A generic error message, given when an unexpected condition has been encountered and no other specific message is appropriate. For a complete article.

list

of

HTTP

response

codes,

see

Wikipedia

We will therefore implement the functional test that verifies access to the Users#show method,

36

test/controllers/api/v1/users_controller_test.rb

# ... class Api::V1::UsersControllerTest < ActionDispatch ::IntegrationTest   setup do   @user = users(:one)   end   test "should show user" do   get api_v1_user_url(@user), as: :json   assert_response :success   # Test to ensure response contains the correct email   json_response = JSON.parse(self.response.body)   assert_equal @user.email, json_response['email']   end end Then simply add the action to our controller. It is extremely simple: app/controllers/api/v1/users_controller.rb

class Api::V1::UsersController < ApplicationController   # GET /users/1   def show   render json: User.find(params[:id])   end end If you run the tests with rails test you get the following error:

$ rails test ...E Error: UsersControllerTest#test_should_show_user: DRb::DRbRemoteError: undefined method `api_v1_user_url' for # (NoMethodError)   test/controllers/users_controller_test.rb:9:in `block in ' This type of error is very common when you generate your resources

37

manually! Indeed, we have totally forgotten the route. So let’s add them: config/routes.rb

Rails.application.routes.draw do   namespace :api, defaults: { format: :json } do   namespace :v1 do   resources :users, only: [:show]   end   end end Tests should now pass:

$ rails test .... 4 runs, 5 assertions, 0 failures, 0 errors, 0 skips As usual, after adding one of the features we are satisfied with, we make a commit:

$ git add . && git commit -m "Adds show action to the users controller"

Test our resource with cURL So we finally have a resource to test. We have several solutions to test it. The first one that comes to mind is the use of cURL, which is integrated in almost all Linux distributions. So let’s try it: First initialize the rails server on a new terminal.

$ rails s Then switch back to your other terminal and run:

$ curl http://localhost:3000/api/v1/users/1 {"id":1,"email":"[email protected]", ... We find the user we created with the Rails console in the previous

38

section. You now have a user registration API entry.

Create users Now that we have a better understanding of how to build entry points, it is time to extend our API. One of the most important features is to let users create a profile on our application. As usual, we will write tests before implementing our code to extend our test suite. Make sure that your Git directory is clean and that you do not have a file in staging. If so commit them so that we can start over. So let’s start by writing our test by adding an entry to create a user on the file users_controller_test.rb: test/controllers/users_controller_test.rb

# ... class Api::V1::UsersControllerTest < ActionDispatch ::IntegrationTest   # ...   test "should create user" do   assert_difference('User.count') do   post api_v1_users_url, params: { user: { email: '[email protected]', password: '123456' } }, as: :json   end   assert_response :created   end   test "should not create user with taken email" do   assert_no_difference('User.count') do   post api_v1_users_url, params: { user: { email: @user .email, password: '123456' } }, as: :json   end   assert_response :unprocessable_entity   end end That’s a lot of code. Don’t worry I’ll explain everything: • In the first test we check the creation of a user by sending a valid POST request. Then, we checked that an additional user exists in the database and that the HTTP code of the response is created (status code 201) • In the second test we check that the user is not created using an

39

email already used. Then, we check that the HTTP response is unprocessable_entity (status code 422)

code

of

the

At that point, the tests must fail (as we expected):

$ rails test ...E So it’s time to implement the code for our tests to be successful: app/controllers/api/v1/users_controller.rb

class Api::V1::UsersController < ApplicationController   # ...   # POST /users   def create   @user = User.new(user_params)   if @user.save   render json: @user, status: :created   else   render json: @user.errors, status: :unprocessable_entity   end   end   private   # Only allow a trusted parameter "white list" through.   def user_params   params.require(:user).permit(:email, :password)   end end Remember that each time we add an entry in our API we must also add this action in our routes.rb file.

40

config/routes.rb

Rails.application.routes.draw do   namespace :api, defaults: { format: :json } do   namespace :v1 do   resources :users, only: %i[show create]   end   end end As you can see, the implementation is quite simple. We have also added the private method user_params to protect mass attribute assignments. Now our tests should pass:

$ rails test ...... 6 runs, 9 assertions, 0 failures, 0 errors, 0 skips Yeah! Let’s commit changes and continue to build our application:

$ git commit -am "Adds the user create endpoint"

Update users The user update scheme is very similar to the one at creation. If you are an experienced Rails developer, you may already know the differences between these two actions: • The update action responds to a PUT/PATCH request. • Only a connected user should be able to update his information. This means that we will have to force a user to authenticate. We will discuss this in Chapter 5. As usual, we start by writing our tests:

41

test/controllers/users_controller_test.rb

# ... class Api::V1::UsersControllerTest < ActionDispatch ::IntegrationTest   # ...   test "should update user" do   patch api_v1_user_url(@user), params: { user: { email: @user.email, password: '123456' } }, as: :json   assert_response :success   end   test "should not update user when invalid params are sent" do   patch api_v1_user_url(@user), params: { user: { email: 'bad_email', password: '123456' } }, as: :json   assert_response :unprocessable_entity   end end For the tests to succeed, we must build the update action on the file users_controller.rb and add the route to the file routes.rb. As you can see, we have too much duplicated code, we will redesign our tests in chapter 4. first we add the action the file routes.rb: config/routes.rb

Rails.application.routes.draw do   # ...   resources :users, only: %i[show create update]   # ... end Then we implement the update action on the user controller and run our tests:

42

app/controllers/api/v1/users_controller.rb

class Api::V1::UsersController < ApplicationController   before_action :set_user, only: %i[show update]   # GET /users/1   def show   render json: @user   end   # ...   # PATCH/PUT /users/1   def update   if @user.update(user_params)   render json: @user, status: :ok   else   render json: @user.errors, status: :unprocessable_entity   end   end   private   # ...   def set_user   @user = User.find(params[:id])   end end All our tests should now pass:

$ rails test ........ 8 runs, 11 assertions, 0 failures, 0 errors, 0 skips We do a commit Since everything works:

$ git commit -am "Adds update action the users controller"

Delete the user So far, we have built a lot of actions on the user controller with

43

their tests but it is not finished. We just need one more, which is the action of destruction. So let’s create the test: test/controllers/users_controller_test.rb

# ... class Api::V1::UsersControllerTest < ActionDispatch ::IntegrationTest   # ...   test "should destroy user" do   assert_difference('User.count', -1) do   delete api_v1_user_url(@user), as: :json   end   assert_response :no_content   end end As you can see, the test is very simple. We only respond with a status of 204 which means No Content. We could also return a status code of 200, but I find it more natural to answer No Content in this case because we delete a resource and a successful response may be enough. The implementation of the destruction action is also quite simple: app/controllers/api/v1/users_controller.rb

class Api::V1::UsersController < ApplicationController   before_action :set_user, only: %i[show update destroy]   # ...   # DELETE /users/1   def destroy   @user.destroy   head 204   end   # ... end Don’t forget to add the action destroy in the file routes.rb:

44

config/routes.rb

Rails.application.routes.draw do   # ...   resources :users, only: %i[show create update destroy]   # ... end Tests should pass if everything is correct:

$ rails test ......... 9 runs, 13 assertions, 0 failures, 0 errors, 0 skips Remember that after making some changes to our code, it is good practice to commit them so that we keep a well-cut history.

$ git commit -am "Adds destroy action to the users controller" And as we get to the end of our chapter, it is time to apply all our modifications to the master branch by doing a merge:

$ git checkout master $ git merge chapter03

45

Conclusion Oh, there you are! Well done! I know it was probably a long time, but don’t give up! Make sure you understand each piece of code, things will improve, in the next chapter, we will redesign our tests to make the code more readable and maintainable. Then stay with me!

46

Authenticating user It’s been a long time since you started. I hope you enjoy this trip as much as I do. In the previous chapter we set up user resource entries. If you have skipped this chapter or if you have not understood everything, I strongly recommend that you look at it. It covers the first bases of the tests and is an introduction to JSON answers. You can clone the project up to this point:

$ git checkout tags/checkpoint_chapter04 In this chapter things will get very interesting because we are going to set up our authentication mechanism. In my opinion it’s one of the most interesting chapters. 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:

$ git checkout -b chapter04

47

Stateless session Before we go any further, something must be clear: an API does not handle sessions. If you don’t have experience building these kind of applications it might sound a little crazy but stay with me. An API should be stateless which means by definition is one that provides a response after your request, and then requires no further attention.. Which means no previous or future state is required for the system to work. The flow for authenticating the user through an API is very simple: 1. The client request for sessions resource with the corresponding credentials (usually email and password) 2. The server returns the user resource along with its corresponding authentication token 3. For 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. 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.

JWT presentation When it comes to authentication tokens, there is a standard: the JSON Web Token (JWT). JWT is an open standard defined in RFC 75191. It allows the secure exchange of tokens between several parties. - Wikipedia Overall a JWT token is composed of three parts: • a header structured in JSON contains for example the validity date of the token. • a payload structured in JSON can contain any data. In our case, it will contain the identifier of the "connected" user. • a signature allows us to verify that the token has been encrypted by

48

our application and is therefore valid. These three parts are each encoded in base64 and then concatenated using points (.). Which gives us something like that: A valid JWT token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIi wibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMe KKF2QT4fwpMeJf36POk6yJV_adQssw5c Once decoded, this token gives us the following information: The JWT token header

{ "alg": "HS256", "typ": "JWT" } The payload of the JWT token

{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

NOTE

For more information about JWT tokens I invite you to visit jwt.io

This has many advantages such as sending information in token’s payload. For example, we may choose to integrate user’s information into the payload.

Setting up the authentication token The JWT standard has many implementations in various languages and libraries. Of course, there is a Ruby gem on this subject: ruby-jwt. So let’s start by installing it:

$ bundle add jwt Once completed the following line is added to your Gemfile:

gem "jwt", "~> 2.2" The library is very simple. There are two methods: JWT.encode and

49

JWT.decode. Let’s open a terminal with console rails and run some tests:

2.6.3 :001 > token = JWT.encode({message: 'Hello World'}, 'my_secret_key') 2.6.3 :002 > JWT.decode(token, 'my_secret_key')  => [{"message"=>"Hello World"}, {"alg"=>"HS256"}] In the first line we encoded a payload with the secret key my_secret_key. So we get a token we can simply decode. The second line decodes the token and we see that we find our payload well. We will now include all this logic in a JsonWebToken class in a new file located in lib/. This will allow us to avoide duplicating the code. This class will just encode and decode the JWT tokens. So here is the implementation. lib/json_web_token.rb

class JsonWebToken   SECRET_KEY = Rails.application.secrets.secret_key_base.to_s   def self.encode(payload, exp = 24.hours.from_now)   payload[:exp] = exp.to_i   JWT.encode(payload, SECRET_KEY)   end   def self.decode(token)   decoded = JWT.decode(token, SECRET_KEY).first   HashWithIndifferentAccess.new decoded   end end I know that’s a lot of code but we’re going to review it together. • the method JsonWebToken.encode takes care of encoding the payload by adding an expiration date of 24 hours by default. We also use the same encryption key as the one configured with Rails • the method JsonWebToken.decode decodes the JWT token and gets the payload. Then we use the HashWithIndifferentAccess class provided by Rails which allows us to retrieve a value of a Hash with a Symbol or String. There you go. In order to load the file into our application, you must specify the lib folder in the list of Ruby on Rails _autoload_s. To do

50

this, add the following configuration to the application.rb file: config/application.rb

# ... module MarketPlaceApi   class Application < Rails::Application   # ...   config.eager_load_paths UserSerializer.new( User.first ).serializable_hash => {:data=>{:id=>"25", :type=>:user, :attributes=>{:email =>"[email protected]"}}} There you go. As you can see this is really easy. Now we can use our new serializer in our controller:

87

app/controllers/api/v1/users_controller.rb

class Api::V1::UsersController < ApplicationController   # ...   def show   render json: UserSerializer.new(@user).serializable_hash   end   def update   if @user.update(user_params)   render json: UserSerializer.new(@user).serializable_hash   else   # ...   end   end   def create   # ...   if @user.save   render json: UserSerializer.new(@user).serializable_hash, status: :created   else   # ...   end   end   # ... end Quite easy isn’t it? However we should have a test who fails. Try it for yourself:

$ rake test Failure: Expected: "[email protected]"   Actual: nil For some reason the answer is not quite what we expect. This is because the gem modifies the response we had previously defined. So to pass these tests you just have to modify it:

88

test/controllers/api/v1/users_controller_test.rb

# ... class Api::V1::UsersControllerTest < ActionDispatch ::IntegrationTest   # ...   test "should show user" do   # ...   assert_equal @user.email, json_response['data'][ 'attributes']['email']   end   # ... end If you do so test now should pass:

$ rake test ........................ Let’s commit these changes and keep moving forward:

$ git add . && git commit -am "Adds user serializer for customizing the json output"

89

Serialize products Now that we understand how the serialization gem works it’s time to customize the product output. The first step is the same as what we did in previous section. We need a product serializer. So let’s do it:

$ rails generate serializer Product title price published   create app/serializers/product_serializer.rb Now let’s add attributes to serialize the product: app/serializers/product_serializer.rb

class ProductSerializer   include FastJsonapi::ObjectSerializer   attributes :title, :price, :published end There you go. It’s no more complicated than that. Let’s change our controller a little bit.

90

app/controllers/api/v1/products_controller.rb

class Api::V1::ProductsController < ApplicationController   # ...   def index   @products = Product.all   render json: ProductSerializer.new(@products ).serializable_hash   end   def show   render json: ProductSerializer.new(@product ).serializable_hash   end   def create   product = current_user.products.build(product_params)   if product.save   render json: ProductSerializer.new(product ).serializable_hash, status: :created   else   # ...   end   end   def update   if @product.update(product_params)   render json: ProductSerializer.new(@product ).serializable_hash   else   # ...   end   end   # ... end And we’re updating our functional test:

91

test/controllers/api/v1/products_controller_test.rb

# ... class Api::V1::ProductsControllerTest < ActionDispatch ::IntegrationTest   # ...   test 'should show product' do   # ...   assert_equal @product.title, json_response['data' ]['attributes']['title']   end   # ... end You can check that tests pass but they should. Let’s commit these small changes:

$ git add . $ git commit -m "Adds product serializer for custom json output"

Serialize associations We have worked with serializers and you may notice that it is very simple. In some cases difficult decision is naming your routes or structuring the JSON output. When working with associations between models on an API there are many approaches you can take. We don’t have to worry about this problem in our case: JSON:API specifications did it for us! To summarize we have a has_many type association between users and products. app/models/user.rb

class User < ApplicationRecord   has_many :products, dependent: :destroy   # ... end

92

app/models/product.rb

class Product < ApplicationRecord   belongs_to :user   # ... end It is a good idea to integrate users into the JSON outputs This will make the output more cumbersome but it will API client from executing other requests to retrieve user related to the products. This method can really save bottleneck.

of products. prevent the information you a huge

93

Theory of the injection of relationships Imagine a scenario where you go to the API to get the products, but in this case you have to display some of the user information. One possible solution would be adding the attribute user_id to the product_serializer so that we can get the corresponding user later. This may sound like a good idea, but if you are concerned about performance, or if your database transactions are not fast enough, you should reconsider this approach. You must understand that for each product you retrieve, you will have to retrieve its corresponding user. Faced with this problem, there are several alternatives.

Integrate into a meta attribute The first solution (a good one in my opinion) is to integrate identifiers of linkded users to products in a meta attribute. So we obtain a JSON like bellow:

{   "meta": { "user_ids": [1,2,3] },   "data": [   ] } So that the client can retrieve these users from these user_ids.

Incorporate the object into the attribute Another solution is to incorporate the user object into the product object. This may make the first request a little slower but in this way the client does not need to make another additional request. An example of the expected results is presented below:

94

{   "data":   [   {   "id": 1,   "type": "product",   "attributes": {   "title": "First product",   "price": "25.02",   "published": false,   "user": {   "id": 2,   "attributes": {   "email": "[email protected]",   "created_at": "2014-07-29T03:52:07.432Z",   "updated_at": "2014-07-29T03:52:07.432Z",   "auth_token": "Xbnzbf3YkquUrF_1bNkZ"   }   }   }   }   ] } The problem with this approach is we have to duplicate the `User' objects for each product that belong to the same user:

95

{   "data":   [   {   "id": 1,   "type": "product",   "attributes": {   "title": "First product",   "price": "25.02",   "published": false,   "user": {   "id": 2,   "type": "user",   "attributes": {   "email": "[email protected]",   "created_at": "2014-07-29T03:52:07.432Z",   "updated_at": "2014-07-29T03:52:07.432Z",   "auth_token": "Xbnzbf3YkquUrF_1bNkZ"   }   }   }   },   {   "id": 2,   "type": "product",   "attributes": {   "title": "Second product",   "price": "25.02",   "published": false,   "user": {   "id": 2,   "type": "user",   "attributes": {   "email": "[email protected]",   "created_at": "2014-07-29T03:52:07.432Z",   "updated_at": "2014-07-29T03:52:07.432Z",   "auth_token": "Xbnzbf3YkquUrF_1bNkZ"   }   }   }   }   ] }

96

Incorporate the relationships into `include The third solution (chosen by the JSON:API) is a mixture of the first two. We will include all the relationships in an include key that will contain all the relationships of the previously mentioned objects. Also, each object will include a relationship key that defines the relationship and that must be found in the include key. A JSON is worth a thousand words:

{   "data":   [   {   "id": 1,   "type": "product",   "attributes": {   "title": "First product",   "price": "25.02",   "published": false   },   "relationships": {   "user": {   "id": 1,   "type": "user"   }   }   },   {   "id": 2,   "type": "product",   "attributes": {   "title": "Second product",   "price": "25.02",   "published": false   },   "relationships": {   "user": {   "id": 1,   "type": "user"   }

97

  }   }   ],   "include": [   {   "id": 2,   "type": "user",   "attributes": {   "email": "[email protected]",   "created_at": "2014-07-29T03:52:07.432Z",   "updated_at": "2014-07-29T03:52:07.432Z",   "auth_token": "Xbnzbf3YkquUrF_1bNkZ"   }   }   ] } Do you see the difference? This solution drastically reduces the size of the JSON and therefore the bandwidth used.

98

Application of the injection of relationships So we will incorporate the user object into the product. Let’s start by adding some tests. We will simply modify the Products#show test to verify that we are recovering: test/controllers/api/v1/products_controller_test.rb

# ... class Api::V1::ProductsControllerTest < ActionDispatch ::IntegrationTest   # ...   test 'should show product' do   get api_v1_product_url(@product), as: :json   assert_response :success   json_response = JSON.parse(response.body, symbolize_names: true)   assert_equal @product.title, json_response.dig(:data, :attributes, :title)   assert_equal @product.user.id.to_s, json_response.dig(:data, :relationships, :user, :data, :id)   assert_equal @product.user.email, json_response.dig (:included, 0, :attributes, :email)   end   # ... end We are now checking three things on the JSON that has been returned: 1. it contains the title of the product 2. it contains the user ID of the user linked to the product 3. the user data is included in the include key

NOTE

You may have noticed that I have chosen to use the method Hash#dig. It is a Ruby method allowing you to retrieve elements in an nested Hash by avoiding errors if an element is not present.

To pass this test we will start by including the relationship in the

99

serializer: app/serializers/product_serializer.rb

class ProductSerializer   include FastJsonapi::ObjectSerializer   attributes :title, :price, :published   belongs_to :user end This addition identifier:

will

add

a

relationship

key

containing

the

user’s

{   "data": {   "id": "1",   "type": "product",   "attributes": {   "title": "Durable Marble Lamp",   "price": "11.55",   "published": true   },   "relationships": {   "user": {   "data": {   "id": "1",   "type": "user"   }   }   }   } } This allows us correcting our first two assertions. We now want to include attributes of the user who owns the product. To do this we simply need to pass an option :include to the serializer instantiated in the controller. Then let’s do it:

100

app/controllers/api/v1/products_controller.rb

class Api::V1::ProductsController < ApplicationController   # ...   def show   options = { include: [:user] }   render json: ProductSerializer.new(@product, options ).serializable_hash   end   # ... end There you go. Now this is what the JSON should look like:

{                         }

"data": { ... }, "included": [ { "id": "1", "type": "user", "attributes": { "email": "[email protected]" } } ]

Now all tests should pass:

$ rake test ........................ Let’s make a commit to celebrate:

$ git commit -am "Add user relationship to product serializer"

101

Retrieve user’s products Do you understand the principle? We have included user information in the JSON of the products. We can do the same by including product information related to a user for the /api/v1/users/1 page. Let’s start with the test: test/controllers/api/v1/users_controller_test.rb

# ... class Api::V1::UsersControllerTest < ActionDispatch ::IntegrationTest   # ...   test "should show user" do   get api_v1_user_url(@user), as: :json   assert_response :success   json_response = JSON.parse(self.response.body, symbolize_names: true)   assert_equal @user.email, json_response.dig(:data, :attributes, :email)   assert_equal @user.products.first.id.to_s, json_response. dig(:data, :relationships, :products, :data, 0, :id)   assert_equal @user.products.first.title, json_response.dig (:included, 0, :attributes, :title)   end   # ... end serializer: app/serializers/user_serializer.rb

class UserSerializer   include FastJsonapi::ObjectSerializer   attributes :email   has_many :products end And to finish controller:

102

app/controllers/api/v1/users_controller.rb

class Api::V1::UsersController < ApplicationController   # ...   def show   options = { include: [:products] }   render json: UserSerializer.new(@user, options ).serializable_hash   end   # ... end There you go. We obtain a JSON like following:

103

{                                                                           }

"data": { "id": "1", "type": "user", "attributes": { "email": "[email protected]" }, "relationships": { "products": { "data": [ { "id": "1", "type": "product" }, { "id": "2", "type": "product" } ] } } }, "included": [ { "id": "1", "type": "product", "attributes": { "title": "Durable Marble Lamp", "price": "11.5537474980286", "published": true }, "relationships": { "user": { "data": { "id": "1", "type": "user" } } } }, { ... } ]

It was really easy. Let’s make a commit:

104

$ git commit -am "Add products relationship to user#show"

105

Search for products In this last section we will continue to strengthen the Products#index action by setting up a very simple search mechanism allowing any customer to filter the results. This section is optional as it will have no impact on the application modules. But if you want to practice more with the TDD I recommend that you to complete this last step. I use Ransack or pg_search to build advanced search forms extremely quickly. But since the goal is learning and searching we are going to do is very simple. I think we can build a search engine from scratch. We simply have to consider the criteria by which we will filter the attributes. Hang on to your seats it’s going to be a tough trip. We will therefore criteria:

filter

the

products

according

to

the

following

• By title • By price • Sort by creation date It may seem short and easy, but believe me, it will give you a headache if you don’t plan it.

The keyword by We will create a scope to find records that character pattern. Let’s call it filter_by_title.

match

a

particular

We will start by adding some fixtures with different products to test:

106

test/fixtures/products.yml

one:   title: TV Plosmo Philopps   price: 9999.99   published: false   user: one two:   title: Azos Zeenbok   price: 499.99   published: false   user: two another_tv:   title: Cheap TV   price: 99.99   published: false   user: two And now we can build some tests: test/models/product_test.rb

# ... class ProductTest < ActiveSupport::TestCase   # ...   test "should filter products by name" do   assert_equal 2, Product.filter_by_title('tv').count   end   test 'should filter products by name and sort them' do   assert_equal [products(:another_tv), products(:one)], Product.filter_by_title('tv').sort   end end The following tests ensure that the method Product.filter_by_title will correctly search for products according to their title. We use the term tv in lowercase to ensure that our search will not be case sensitive.

107

app/models/product.rb

class Product < ApplicationRecord   # ...   scope :filter_by_title, lambda { |keyword|   where('lower(title) LIKE ?', "%#{keyword.downcase}%")   } end

NOTE

scoping allows you to specify commonly used queries that can be referenced as method calls on models. With these scopes you can also link with Active Record methods like where, joins and includes because a scope always returns an object ActiveRecord::Relation. I invite you taking a look at Rails documentation

Implementation is sufficient for our tests to pass:

$ rake test ..........................

By price To filter by price, things can get a little more delicate. We will break the logic of filtering by price in two different methods: one that will look for products larger than the price received and the other that will look for those that are below that price. This way, we will keep some flexibility and we can easily test the scope. Let’s start by building the tests of the scope above_or_equal_to_price: test/models/product_test.rb

# ... class ProductTest < ActiveSupport::TestCase   # ...   test 'should filter products by price and sort them' do   assert_equal [products(:two), products(:one)], Product .above_or_equal_to_price(200).sort   end end Implementation is very, very simple:

108

app/models/product.rb

class Product < ApplicationRecord   # ...   scope :above_or_equal_to_price, lambda { |price|   where('price >= ?', price)   } end This is sufficient to convert our tests to green:

$ rake test ........................... You can now imagine the behaviour of the opposite method. Here are the tests: test/models/product_test.rb

# ... class ProductTest < ActiveSupport::TestCase   # ...   test 'should filter products by price lower and sort them' do   assert_equal [products(:another_tv)], Product .below_or_equal_to_price(200).sort   end end and the implementation. app/models/product.rb

class Product < ApplicationRecord   # ...   scope :below_or_equal_to_price, lambda { |price|   where('price