Simplify - Articles tagged with Minitest
Personal website of Saša Jovanić
https://www.simplify.ba/articles/tags/minitest/
2021-12-17T19:12:00+01:00
Saša Jovanić
Creating Rails 5 API only application following JSON:API specification
https://www.simplify.ba/articles/2016/06/18/creating-rails5-api-only-application-following-jsonapi-specification/
2016-06-18T15:41:12+02:00
2017-01-08T10:55:17+01:00
Saša Jovanić
<p>This article describes how to build <abbr title="Application Programming Interface">API</abbr> only Rails application using new Rails 5 <code>--api</code> option. Further, I'll explain how to follow <a href="http://jsonapi.org/"><abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr></a> specification in your code and how to test your <abbr title="Application Programming Interface">API</abbr>'s. Also, I'll cover token authentication using some of the new Rails 5 features. All code from this article is <a href="https://github.com/Simplify/rails5_json_api_demo">available on GitHub</a>.</p>
<h2 id="rails-5-api">Rails 5 <abbr title="Application Programming Interface">API</abbr></h2>
<p>Rails 5 have new <code>--api</code> flag that you can use when creating new application. That will create lightweight Rails application suitable for serving only <abbr title="Application Programming Interface">API</abbr> data. This was originally started as separated gem called <a href="https://github.com/rails-api/rails-api/">rails-api</a> but now is part of Rails 5.</p>
<h2 id="jsonapi"><abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr></h2>
<p><a href="http://jsonapi.org/"><abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr></a> is specification that defines how server had to deliver <abbr title="JavaScript Object Notation">JSON</abbr> response, how client should format request, how to implement filtering, sorting, pagination, handle errors, describe relationship between data, <abbr title="The Hypertext Transfer Protocol">HTTP</abbr> status codes that server need to return and other things. Specification is originally extracted from ember-data library, but you can find <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> libraries for different languages and frameworks.</p>
<h2 id="application">Application</h2>
<p>In order to show most of the features described in this article, I'm going to build simple "blog" style application where all registered users can post articles. To keep everything simple, this is not going to be full application. I'll implement users, authentication and articles, but not commenting on articles. Also, I'm not going to implement any "blog" features that have little or nothing to do with the topic of this article, for instance password resetting, user authorization based on roles, etc…</p>
<h2 id="installing-rails-5-and-generating-api-only-application">Installing Rails 5 and generating <abbr title="Application Programming Interface">API</abbr> only application</h2>
<p>Install Rails 5:</p>
<pre class="highlight plaintext"><code>gem install rails
</code></pre>
<p>After that, you can generate new <abbr title="Application Programming Interface">API</abbr> only application using:</p>
<pre class="highlight plaintext"><code>rails new rails5_json_api_demo --api
</code></pre>
<p>Please, check other options that rails provides with <code>rails -h</code>. For example you can use <code>-C</code> to skip generation of ActionCable (web sockets) files, <code>-M</code> to skip ActionMailer, etc…</p>
<h2 id="enabling-cors">Enabling <abbr title="Cross-Origin Resource Sharing">CORS</abbr></h2>
<p><a href="https://www.w3.org/TR/cors/"><abbr title="Cross-Origin Resource Sharing">CORS</abbr></a> is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the resource originated. Application generated with <code>--api</code> will generate <abbr title="Cross-Origin Resource Sharing">CORS</abbr> initializer, but <code>rack-cors</code> gem is still disabled in generated Gemfile. Enable it there and then run <code>bundle</code> to install it.</p>
<p>After that you need to enable <abbr title="Cross-Origin Resource Sharing">CORS</abbr> in <code>config/initializers/cors.rb</code> file and edit it for your needs. You can find some examples on <code>rack-cors</code> <a href="https://github.com/cyu/rack-cors/">GitHub repo</a>. For this example I'll use defaults from generated file, with exception of <code>origins</code>. I'll remove <code>example.com</code> and place <code>*</code> instead. In this case I don't care where request came from.</p>
<p>Alternative for <abbr title="Cross-Origin Resource Sharing">CORS</abbr> is <a href="https://en.wikipedia.org/wiki/JSONP"><abbr title="JSON with Padding">JSON-P</abbr></a>, however I'll not cover it here. <abbr title="Cross-Origin Resource Sharing">CORS</abbr> is endorsed by Rails and <abbr title="World Wide Web Consortium">W3C</abbr> recommendation and I'll like to stick with Rails defaults. Also, <abbr title="JSON with Padding">JSON-P</abbr> supports only GET request method and that is not enough for this application.</p>
<h2 id="serialization">Serialization</h2>
<p>Even Rails provides <abbr title="JavaScript Object Notation">JSON</abbr> serialization, I'll use <a href="https://github.com/rails-api/active_model_serializers">active_model_serializers</a> gem. "JsonApi Adapter" provided by this gem will save me a lot of time.</p>
<p>Add following to Gemfile:</p>
<pre class="highlight ruby"><code><span class="n">gem</span> <span class="s1">'active_model_serializers'</span><span class="p">,</span> <span class="s1">'~> 0.10.0'</span>
</code></pre>
<p>Run <code>bundle</code> to install this gem. After that create <code>config/initializers/active_model_serializers.rb</code> file and write following in there:</p>
<pre class="highlight ruby"><code><span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">adapter</span> <span class="o">=</span> <span class="ss">:json_api</span>
</code></pre>
<p>We will also use Rails <abbr title="Universal Resource Locator">URL</abbr> helpers for link generation and have to specify default <abbr title="Universal Resource Locator">URL</abbr> options in environment files. Here is what I added in <code>development.rb</code> and <code>test.rb</code> files in <code>config/environments/</code> directory:</p>
<pre class="highlight ruby"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">default_url_options</span> <span class="o">=</span> <span class="p">{</span>
<span class="ss">host: </span><span class="s1">'localhost'</span><span class="p">,</span>
<span class="ss">port: </span><span class="mi">3000</span>
<span class="p">}</span>
</code></pre>
<h2 id="creating-users">Creating users</h2>
<p>One of important aspects when creating users is authentication. In demo application only authenticated users can create or edit posts. I'll use token authentication with <a href="http://docs.rubydocs.org/rails-5-0-0-rc1/classes/ActiveRecord/SecureToken/ClassMethods.html">has_secure_token</a> method in Rails 5. As alternative, you can use Devise gem with build in token authentication or use it with <a href="https://www.sitepoint.com/introduction-to-using-jwt-in-rails/"><abbr title="JSON Web Token">JWT</abbr></a>. Personally, I usually use Devise, but in this case I need something simple and want to explore this new <code>has_secure_token</code> method.</p>
<p>Let start with a migration. Run <code>rails g migration CreateUsers</code> and edit new file like this:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">CreateUsers</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mi">5</span><span class="o">.</span><span class="mi">0</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:users</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:full_name</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:password_digest</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:token</span>
<span class="n">t</span><span class="p">.</span><span class="nf">text</span> <span class="ss">:description</span>
<span class="k">end</span>
<span class="n">add_index</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">:token</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>One of changes in Rails 5 is that migrations are now versioned. Other change is that you don't run migrations with <code>rake</code> any more. Just replace <code>rake</code> with <code>rails</code> when trying to run any task. To create this table in database run <code>rails db:migrate</code>.</p>
<p>Create model <code>app/models/user.rb</code>:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_secure_token</span>
<span class="n">has_secure_password</span>
<span class="n">validates</span> <span class="ss">:full_name</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
<span class="k">end</span>
</code></pre>
<p>Add new route:</p>
<pre class="highlight ruby"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:users</span>
<span class="k">end</span>
</code></pre>
<p>When adding <code>resources</code> into routes of Rails 5 application created with <code>--api</code>, routes for <code>new</code> and <code>edit</code> are not created. Exactly what we want.</p>
<p>Next step is to create serializer for our model. Create file <code>app/serializers/user_serializer.rb</code> and add following:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">UserSerializer</span> <span class="o"><</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span>
<span class="n">attributes</span> <span class="ss">:id</span><span class="p">,</span> <span class="ss">:full_name</span><span class="p">,</span> <span class="ss">:description</span><span class="p">,</span> <span class="ss">:created_at</span>
<span class="k">end</span>
</code></pre>
<p>This basically say's that when requesting user data, we'll get only <code>id</code>, <code>full_name</code>, <code>created_at</code> and <code>description</code>.</p>
<p>Last step is creating users controller. Create file <code>app/controllers/users_controller.rb</code> and add following in that file:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="n">users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">all</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">users</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>As you see, there is nothing special in this controller. ActiveModel::Serializers integrates fully into Rails controllers.</p>
<p>Add new user in Rails console (<code>rails c</code>):</p>
<pre class="highlight ruby"><code><span class="no">User</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">full_name: </span><span class="s2">"Sasa J"</span><span class="p">,</span> <span class="ss">password: </span><span class="s2">"Test"</span><span class="p">)</span>
</code></pre>
<p>Start server with <code>rails s</code> and check http://localhost:3000/users. You'll see something like this (indentation added by me):</p>
<pre class="highlight json"><code><span class="p">{</span><span class="nt">"data"</span><span class="p">:[</span><span class="w">
</span><span class="p">{</span><span class="nt">"id"</span><span class="p">:</span><span class="s2">"1"</span><span class="p">,</span><span class="w">
</span><span class="nt">"type"</span><span class="p">:</span><span class="s2">"users"</span><span class="p">,</span><span class="w">
</span><span class="nt">"attributes"</span><span class="p">:{</span><span class="w">
</span><span class="nt">"full-name"</span><span class="p">:</span><span class="s2">"Sasa J"</span><span class="p">,</span><span class="w">
</span><span class="nt">"description"</span><span class="p">:</span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nt">"created-at"</span><span class="p">:</span><span class="s2">"2016-06-16T09:55:37.856Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]}</span><span class="w">
</span></code></pre>
<p>This is exactly what <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> output needs to be. Not only that <code>active_model_serializers</code> gem did wrap everything into <code>data</code>, add <code>type</code>, made separation between <code>id</code> and the rest of attributes, but also replaced underscores with dashes, as <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> requires.</p>
<h2 id="media-type">Media type</h2>
<p>If you open development tools in your browser and check "Content-Type" response from server, you'll notice <code>application/json</code>. <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> requires that we use <code>application/vnd.api+json</code>. Lets change it! Open <code>config/initializers/mime_types.rb</code> and add following line at the end:</p>
<pre class="highlight ruby"><code><span class="no">Mime</span><span class="o">::</span><span class="no">Type</span><span class="p">.</span><span class="nf">register</span> <span class="s2">"application/vnd.api+json"</span><span class="p">,</span> <span class="ss">:json</span>
</code></pre>
<p>That will solve this problem. If you are using Firefox you may get dialog that prompts you to download file <code>users</code>. You can fix this by adding this media type in Firefox or simply use browser that understands <code>application/vnd.api+json</code>, for example Chrome. Even better, you can install browser extension that allows you to make <abbr title="Application Programming Interface">API</abbr> requests (Postman for Chrome or RESTED for Firefox).</p>
<h2 id="adding-basis-for-posts">Adding basis for posts</h2>
<p>Let's create migration for posts with <code>rails g migration CreatePosts</code> and edit newly created file:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">CreatePosts</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mi">5</span><span class="o">.</span><span class="mi">0</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:posts</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:title</span>
<span class="n">t</span><span class="p">.</span><span class="nf">text</span> <span class="ss">:content</span>
<span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:user_id</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:category</span>
<span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:rating</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>Run this migration with <code>rails db:migrate</code> and then create model for posts <code>app/models/post.rb</code>:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="ss">:user</span>
<span class="k">end</span>
</code></pre>
<p>Don't forget to add <code>has_many :posts, dependent: :destroy</code> to <code>user.rb</code> model!</p>
<p>Add <code>resources :posts</code> in <code>config/routes.rb</code> file.</p>
<p>Next step is to create serializer for posts. Create file <code>app/serializers/post_serializer.rb</code> with following code:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">PostSerializer</span> <span class="o"><</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span>
<span class="n">attributes</span> <span class="ss">:id</span><span class="p">,</span> <span class="ss">:title</span><span class="p">,</span> <span class="ss">:content</span><span class="p">,</span> <span class="ss">:category</span><span class="p">,</span>
<span class="ss">:rating</span><span class="p">,</span> <span class="ss">:created_at</span><span class="p">,</span> <span class="ss">:updated_at</span>
<span class="n">belongs_to</span> <span class="ss">:user</span>
<span class="k">end</span>
</code></pre>
<p>As you see, ActiveModel::Serializer provides exactly the same way to describe relationships as ActiveRecord. Don't forget to add <code>has_many :posts</code> in UserSerializer!</p>
<p>If you create associated post record in database and check /users/ <abbr title="Universal Resource Locator">URL</abbr> again, you'll see something like this:</p>
<pre class="highlight json"><code><span class="p">{</span><span class="nt">"data"</span><span class="p">:[{</span><span class="w">
</span><span class="nt">"id"</span><span class="p">:</span><span class="s2">"1"</span><span class="p">,</span><span class="w">
</span><span class="nt">"type"</span><span class="p">:</span><span class="s2">"users"</span><span class="p">,</span><span class="w">
</span><span class="nt">"attributes"</span><span class="p">:{</span><span class="w">
</span><span class="nt">"full-name"</span><span class="p">:</span><span class="s2">"Sasa J"</span><span class="p">,</span><span class="w">
</span><span class="nt">"description"</span><span class="p">:</span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nt">"created-at"</span><span class="p">:</span><span class="s2">"2016-06-16T09:55:37.856Z"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nt">"relationships"</span><span class="p">:{</span><span class="w">
</span><span class="nt">"posts"</span><span class="p">:{</span><span class="w">
</span><span class="nt">"data"</span><span class="p">:[{</span><span class="w">
</span><span class="nt">"id"</span><span class="p">:</span><span class="s2">"1"</span><span class="p">,</span><span class="w">
</span><span class="nt">"type"</span><span class="p">:</span><span class="s2">"posts"</span><span class="w">
</span><span class="p">}]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
<p>As you can see, relationships are part of <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> and ActiveModel::Serializer does great job here.</p>
<h2 id="providing-links-in-json-data">Providing links in <abbr title="JavaScript Object Notation">JSON</abbr> data</h2>
<p><abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> allows us to provide <code>links</code> to resources in separated links <abbr title="JavaScript Object Notation">JSON</abbr> object. Client can use those to fetch more data. Add <code>links</code> into UserSerializer:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">UserSerializer</span> <span class="o"><</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span>
<span class="n">attributes</span> <span class="ss">:id</span><span class="p">,</span> <span class="ss">:full_name</span><span class="p">,</span> <span class="ss">:description</span><span class="p">,</span> <span class="ss">:created_at</span>
<span class="n">has_many</span> <span class="ss">:posts</span>
<span class="n">link</span><span class="p">(</span><span class="ss">:self</span><span class="p">)</span> <span class="p">{</span> <span class="n">user_url</span><span class="p">(</span><span class="n">object</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre>
<p>If you check <code>/users/</code> <abbr title="Universal Resource Locator">URL</abbr> in browser, you'll notice <code>links</code> block.</p>
<h2 id="fixtures">Fixtures</h2>
<p>In most cases you write tests first, but here I decided to make everything working a bit before creating full implementation with tests. It's also better to explain a few concepts of Rails 5 and <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> before going into <abbr title="Test Driven Development">TDD</abbr>.</p>
<p>I'm going to use Minitest in this case. It's included in Rails for some time, fast and I like it more than RSpec. Also Rails 5 brings some Minitest goodies that I'll like to try.</p>
<p>First, lets create fixtures:</p>
<pre class="highlight ruby"><code><span class="c1"># users.yml</span>
<span class="o"><</span><span class="sx">% 6.times </span><span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="sx">%>
user_<%= i %></span><span class="p">:</span>
<span class="ss">full_name: </span><span class="o"><</span><span class="sx">%= "User Nr#{i}" %>
password_digest: <%=</span> <span class="no">BCrypt</span><span class="o">::</span><span class="no">Password</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="s1">'password'</span><span class="p">)</span> <span class="o">%></span>
<span class="ss">token: </span><span class="o"><</span><span class="sx">%= SecureRandom.base58(24) %>
<% end %>
</span></code></pre>
<pre class="highlight ruby"><code><span class="c1"># posts.yml</span>
<span class="o"><</span><span class="sx">% 6.times </span><span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="sx">%>
<% 25.times do |n| %></span>
<span class="n">article_</span><span class="o"><</span><span class="sx">%= i %>_<%=</span> <span class="n">n</span> <span class="sx">%>:
title: <%= "Example title #{i}/#{n}" %></span>
<span class="ss">content: </span><span class="o"><</span><span class="sx">%= "Example content #{i}/#{n}" %>
user: <%=</span> <span class="s2">"user_</span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="s2">"</span> <span class="o">%></span>
<span class="ss">rating: </span><span class="o"><</span><span class="sx">%= 1 + i + rand(3) %>
category: <%=</span> <span class="n">i</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">?</span> <span class="s1">'First'</span> <span class="p">:</span> <span class="s1">'Example'</span> <span class="o">%></span>
<span class="o"><</span><span class="sx">% end </span><span class="o">%></span>
<span class="o"><</span><span class="sx">% end </span><span class="o">%></span>
</code></pre>
<p>This will create 6 users and 150 posts, 25 for each user. I added some variations in <code>rating</code> and <code>category</code> fields in order to test filtering and sorting.</p>
<h2 id="adding-tests-for-index-and-show-action-on-users-controller">Adding tests for index and show action on users controller</h2>
<p>Ok, lets write some controller tests in <code>test/controllers/users_controller_test.rb</code>:</p>
<pre class="highlight ruby"><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="nb">require</span> <span class="s1">'json'</span>
<span class="k">class</span> <span class="nc">UsersControllerTest</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">TestCase</span>
<span class="nb">test</span> <span class="s2">"Should get valid list of users"</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:index</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">assert_equal</span> <span class="n">response</span><span class="p">.</span><span class="nf">content_type</span><span class="p">,</span> <span class="s1">'application/vnd.api+json'</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="mi">6</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">].</span><span class="nf">length</span>
<span class="n">assert_equal</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">'type'</span><span class="p">],</span> <span class="s1">'users'</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"Should get valid user data"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_1'</span><span class="p">)</span>
<span class="n">get</span> <span class="ss">:show</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">.</span><span class="nf">to_s</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'id'</span><span class="p">]</span>
<span class="n">assert_equal</span> <span class="n">user</span><span class="p">.</span><span class="nf">full_name</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'attributes'</span><span class="p">][</span><span class="s1">'full-name'</span><span class="p">]</span>
<span class="n">assert_equal</span> <span class="n">user_url</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="p">{</span> <span class="ss">host: </span><span class="s2">"localhost"</span><span class="p">,</span> <span class="ss">port: </span><span class="mi">3000</span> <span class="p">}),</span>
<span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'links'</span><span class="p">][</span><span class="s1">'self'</span><span class="p">]</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"Should get JSON:API error block when requesting user data with invalid ID"</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:show</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">id: </span><span class="s2">"z"</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="mi">404</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="s2">"Wrong ID provided"</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'errors'</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">'detail'</span><span class="p">]</span>
<span class="n">assert_equal</span> <span class="s1">'/data/attributes/id'</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'errors'</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">'source'</span><span class="p">][</span><span class="s1">'pointer'</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>When writing tests, most important part is to know what to test.</p>
<ul>
<li>Header <code>Content-Type</code> in response is set globally for all responses and that means that I need to test that just once in complete suite.</li>
<li>When getting users list I only care that there are 6 users and that any (in this case first) of those 6 user data blocks have type set to <code>users</code>. I don't need to test data for any of those users. Same serializer that creates data for those 6 users is also used when requesting single user.</li>
<li>When getting single user data, I'll check if data is correct. If <code>id</code> and <code>full_name</code> are correct, the rest is correct too.</li>
<li>Link is something that I have added separately and that is something that needs to become tested. For some reason providing <code>default_url_options</code> in <code>config/environments/test.rb</code> works for <abbr title="The Hypertext Transfer Protocol">HTTP</abbr> requests in tests but not in <abbr title="Universal Resource Locator">URL</abbr> generation. That is the reason that I had to specify host and port directly in the test.</li>
<li>I also had to create test for <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> errors. In this case I'm sending error about wrong ID and pointing to <code>id</code> attribute.</li>
</ul>
<p>This is how users controller end up:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="ss">:set_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:show</span><span class="p">,</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:destroy</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="n">users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">all</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">users</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="n">render</span> <span class="ss">json: </span><span class="vi">@user</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">set_user</span>
<span class="k">begin</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span> <span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">]</span>
<span class="k">rescue</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">RecordNotFound</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span>
<span class="n">user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="ss">:id</span><span class="p">,</span> <span class="s2">"Wrong ID provided"</span><span class="p">)</span>
<span class="n">render_error</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="mi">404</span><span class="p">)</span> <span class="n">and</span> <span class="k">return</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>I placed error rendering in ApplicationController:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">API</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">render_error</span><span class="p">(</span><span class="n">resource</span><span class="p">,</span> <span class="n">status</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">resource</span><span class="p">,</span> <span class="ss">status: </span><span class="n">status</span><span class="p">,</span> <span class="ss">adapter: :json_api</span><span class="p">,</span>
<span class="ss">serializer: </span><span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span><span class="o">::</span><span class="no">ErrorSerializer</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>Errors are described in more details on <a href="https://github.com/rails-api/active_model_serializers/blob/master/docs/jsonapi/errors.md">active_model_serializer <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> errors document</a> and <a href="http://jsonapi.org/format/#errors">errors part of <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> spec</a>.</p>
<h2 id="creating-new-user">Creating new user</h2>
<ul>
<li>When creating or updating users we need authentication. As said before, we'll use token authentication.</li>
<li>When sending <abbr title="JavaScript Object Notation">JSON</abbr> data to server we need to specify <code>Content-Type</code> header. When using <abbr title="The Hypertext Transfer Protocol">HTTP</abbr> methods that do not send any <abbr title="JavaScript Object Notation">JSON</abbr> data (GET and DELETE) we <a href="https://github.com/emberjs/data/issues/4226">don't need to set Content-Type header</a>.</li>
<li><code>type</code> must be present and correct in <abbr title="JavaScript Object Notation">JSON</abbr> data.</li>
</ul>
<p>This is how new set of tests looks like:</p>
<pre class="highlight ruby"><code> <span class="nb">test</span> <span class="s2">"Creating new user without sending correct content-type should result in error"</span> <span class="k">do</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{}</span>
<span class="n">assert_response</span> <span class="mi">406</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"Creating new user without sending X-Api-Key should result in error"</span> <span class="k">do</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'application/vnd.api+json'</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{}</span>
<span class="n">assert_response</span> <span class="mi">403</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"Creating new user with incorrect X-Api-Key should result in error"</span> <span class="k">do</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'application/vnd.api+json'</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"X-Api-Key"</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'0000'</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{}</span>
<span class="n">assert_response</span> <span class="mi">403</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"Creating new user with invalid type in JSON data should result in error"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_1'</span><span class="p">)</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'application/vnd.api+json'</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"X-Api-Key"</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">token</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">type: </span><span class="s1">'posts'</span> <span class="p">}}</span>
<span class="n">assert_response</span> <span class="mi">409</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"Creating new user with invalid data should result in error"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_1'</span><span class="p">)</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'application/vnd.api+json'</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"X-Api-Key"</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">token</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span>
<span class="ss">data: </span><span class="p">{</span>
<span class="ss">type: </span><span class="s1">'users'</span><span class="p">,</span>
<span class="ss">attributes: </span><span class="p">{</span>
<span class="ss">full_name: </span><span class="kp">nil</span><span class="p">,</span>
<span class="ss">password: </span><span class="kp">nil</span><span class="p">,</span>
<span class="ss">password_confirmation: </span><span class="kp">nil</span> <span class="p">}}}</span>
<span class="n">assert_response</span> <span class="mi">422</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">pointers</span> <span class="o">=</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'errors'</span><span class="p">].</span><span class="nf">collect</span> <span class="p">{</span> <span class="o">|</span><span class="n">e</span><span class="o">|</span>
<span class="n">e</span><span class="p">[</span><span class="s1">'source'</span><span class="p">][</span><span class="s1">'pointer'</span><span class="p">].</span><span class="nf">split</span><span class="p">(</span><span class="s1">'/'</span><span class="p">).</span><span class="nf">last</span>
<span class="p">}.</span><span class="nf">sort</span>
<span class="n">assert_equal</span> <span class="p">[</span><span class="s1">'full-name'</span><span class="p">,</span><span class="s1">'password'</span><span class="p">],</span> <span class="n">pointers</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"Creating new user with valid data should create new user"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_1'</span><span class="p">)</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'application/vnd.api+json'</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"X-Api-Key"</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">token</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span>
<span class="ss">data: </span><span class="p">{</span>
<span class="ss">type: </span><span class="s1">'users'</span><span class="p">,</span>
<span class="ss">attributes: </span><span class="p">{</span>
<span class="ss">full_name: </span><span class="s1">'User Number7'</span><span class="p">,</span>
<span class="ss">password: </span><span class="s1">'password'</span><span class="p">,</span>
<span class="ss">password_confirmation: </span><span class="s1">'password'</span> <span class="p">}}}</span>
<span class="n">assert_response</span> <span class="mi">201</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="s1">'User Number7'</span><span class="p">,</span>
<span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'attributes'</span><span class="p">][</span><span class="s1">'full-name'</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
<p>New additions to UsersController:</p>
<pre class="highlight ruby"><code> <span class="n">before_action</span> <span class="ss">:validate_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:destroy</span><span class="p">]</span>
<span class="n">before_action</span> <span class="ss">:validate_type</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">save</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">user</span><span class="p">,</span> <span class="ss">status: :created</span>
<span class="k">else</span>
<span class="n">render_error</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="ss">:unprocessable_entity</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">user_params</span>
<span class="no">ActiveModelSerializers</span><span class="o">::</span><span class="no">Deserialization</span><span class="p">.</span><span class="nf">jsonapi_parse</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>And this is how ApplicationController looks now:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">API</span>
<span class="n">before_action</span> <span class="ss">:check_header</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">check_header</span>
<span class="k">if</span> <span class="p">[</span><span class="s1">'POST'</span><span class="p">,</span><span class="s1">'PUT'</span><span class="p">,</span><span class="s1">'PATCH'</span><span class="p">].</span><span class="nf">include?</span> <span class="n">request</span><span class="p">.</span><span class="nf">method</span>
<span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="nf">content_type</span> <span class="o">!=</span> <span class="s2">"application/vnd.api+json"</span>
<span class="n">head</span> <span class="mi">406</span> <span class="n">and</span> <span class="k">return</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">validate_type</span>
<span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="s1">'data'</span><span class="p">]</span> <span class="o">&&</span> <span class="n">params</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'type'</span><span class="p">]</span>
<span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'type'</span><span class="p">]</span> <span class="o">==</span> <span class="n">params</span><span class="p">[</span><span class="ss">:controller</span><span class="p">]</span>
<span class="k">return</span> <span class="kp">true</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">head</span> <span class="mi">409</span> <span class="n">and</span> <span class="k">return</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">validate_user</span>
<span class="n">token</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"X-Api-Key"</span><span class="p">]</span>
<span class="n">head</span> <span class="mi">403</span> <span class="n">and</span> <span class="k">return</span> <span class="k">unless</span> <span class="n">token</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span> <span class="ss">token: </span><span class="n">token</span>
<span class="n">head</span> <span class="mi">403</span> <span class="n">and</span> <span class="k">return</span> <span class="k">unless</span> <span class="n">user</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">render_error</span><span class="p">(</span><span class="n">resource</span><span class="p">,</span> <span class="n">status</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">resource</span><span class="p">,</span> <span class="ss">status: </span><span class="n">status</span><span class="p">,</span> <span class="ss">adapter: :json_api</span><span class="p">,</span> <span class="ss">serializer: </span><span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span><span class="o">::</span><span class="no">ErrorSerializer</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2 id="updating-user">Updating user</h2>
<p>Most of the rules about sending data when updating an user are same as when creating one. There is no point for writing extra tests for headers checks and validity of the data because <code>before_action</code> used for <code>create</code> are the same as for <code>update</code>. Validations are also the same. I'll just write test for successful <code>update</code> action:</p>
<pre class="highlight ruby"><code> <span class="nb">test</span> <span class="s2">"Updating an existing user with valid data should update that user"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_1'</span><span class="p">)</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'application/vnd.api+json'</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"X-Api-Key"</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">token</span>
<span class="n">patch</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span>
<span class="ss">id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span>
<span class="ss">data: </span><span class="p">{</span>
<span class="ss">id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span>
<span class="ss">type: </span><span class="s1">'users'</span><span class="p">,</span>
<span class="ss">attributes: </span><span class="p">{</span> <span class="ss">full_name: </span><span class="s1">'User Number1a'</span> <span class="p">}}}</span>
<span class="n">assert_response</span> <span class="mi">200</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="s1">'User Number1a'</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'attributes'</span><span class="p">][</span><span class="s1">'full-name'</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
<p>And update action in UsersController:</p>
<pre class="highlight ruby"><code> <span class="k">def</span> <span class="nf">update</span>
<span class="k">if</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">update_attributes</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="vi">@user</span><span class="p">,</span> <span class="ss">status: :ok</span>
<span class="k">else</span>
<span class="n">render_error</span><span class="p">(</span><span class="vi">@user</span><span class="p">,</span> <span class="ss">:unprocessable_entity</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2 id="deleting-user">Deleting user</h2>
<p>This one is simple, just delete record and return only headers with status 204 "No Content". Test is here:</p>
<pre class="highlight ruby"><code> <span class="nb">test</span> <span class="s2">"Should delete user"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_1'</span><span class="p">)</span>
<span class="n">ucount</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">count</span> <span class="o">-</span> <span class="mi">1</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"X-Api-Key"</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">token</span>
<span class="n">delete</span> <span class="ss">:destroy</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">id: </span><span class="n">users</span><span class="p">(</span><span class="s1">'user_5'</span><span class="p">).</span><span class="nf">id</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="mi">204</span>
<span class="n">assert_equal</span> <span class="n">ucount</span><span class="p">,</span> <span class="no">User</span><span class="p">.</span><span class="nf">count</span>
<span class="k">end</span>
</code></pre>
<p>And <code>destroy</code> action in UsersController:</p>
<pre class="highlight ruby"><code> <span class="k">def</span> <span class="nf">destroy</span>
<span class="vi">@user</span><span class="p">.</span><span class="nf">destroy</span>
<span class="n">head</span> <span class="mi">204</span>
<span class="k">end</span>
</code></pre>
<h2 id="posts">Posts</h2>
<p>Ok, after implementing users it's time to do posts. I'm going to implement only <code>index</code> method in this article, the rest of the methods are similar to those implemented in UsersControllers. You can find them in <a href="https://github.com/Simplify/rails5_json_api_demo">this demo application on GitHub</a>. The reason that I'm doing <code>index</code> method in this article is because that is the best place to implement sorting, filtering and ordering following <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> spec.</p>
<p>Lets start with test in <code>test/controllers/posts_controller_test.rb</code>:</p>
<pre class="highlight ruby"><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="nb">require</span> <span class="s1">'json'</span>
<span class="k">class</span> <span class="nc">PostsControllerTest</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">TestCase</span>
<span class="nb">test</span> <span class="s2">"Should get valid list of posts"</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:index</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="no">Post</span><span class="p">.</span><span class="nf">count</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">].</span><span class="nf">length</span>
<span class="n">assert_equal</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">'type'</span><span class="p">],</span> <span class="s1">'posts'</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>And <code>app/controllers/posts_controller.rb</code>:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="n">posts</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">all</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">posts</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2 id="pagination">Pagination</h2>
<p><code>active_model_serializers</code> gem provides pagination for us by using <code>kaminari</code> or <code>will_paginate</code> gems. I'll use <code>will_paginate</code>.</p>
<p>Add <code>gem "will_paginate"</code> in your <code>Gemfile</code> and run <code>bundle</code>.</p>
<p>Modify Post model:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="ss">:user</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">per_page</span> <span class="o">=</span> <span class="mi">50</span>
<span class="k">end</span>
</code></pre>
<p>After that add pagination in PostsController:</p>
<pre class="highlight ruby"><code> <span class="k">def</span> <span class="nf">index</span>
<span class="n">posts</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">page</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:page</span><span class="p">]</span> <span class="p">?</span> <span class="n">params</span><span class="p">[:</span><span class="n">page</span><span class="p">][</span><span class="ss">:number</span><span class="p">]</span> <span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">posts</span>
<span class="k">end</span>
</code></pre>
<p>This will cause test to fail:</p>
<pre class="highlight plaintext"><code>Failure:
PostsControllerTest#test_Should_get_valid_list_of_posts [<filename-removed>:10]:
Expected: 150
Actual: 50
</code></pre>
<p>We can fix this by changing our test:</p>
<pre class="highlight ruby"><code> <span class="nb">test</span> <span class="s2">"Should get valid list of posts"</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:index</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">page: </span><span class="p">{</span> <span class="ss">number: </span><span class="mi">2</span> <span class="p">}</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="no">Post</span><span class="p">.</span><span class="nf">per_page</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">].</span><span class="nf">length</span>
<span class="n">assert_equal</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">'type'</span><span class="p">],</span> <span class="s1">'posts'</span>
<span class="k">end</span>
</code></pre>
<p><abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> also suggests that you may add links for other pages if you use pagination. In that case you are required to use names <code>first</code>, <code>prev</code>, <code>next</code> and <code>last</code> for those links. That is also generated for us, so lets test it:</p>
<pre class="highlight ruby"><code> <span class="nb">test</span> <span class="s2">"Should get valid list of posts"</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:index</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">page: </span><span class="p">{</span> <span class="ss">number: </span><span class="mi">2</span> <span class="p">}</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="no">Post</span><span class="p">.</span><span class="nf">per_page</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">].</span><span class="nf">length</span>
<span class="n">assert_equal</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">'type'</span><span class="p">],</span> <span class="s1">'posts'</span>
<span class="n">l</span> <span class="o">=</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'links'</span><span class="p">]</span>
<span class="n">assert_equal</span> <span class="n">l</span><span class="p">[</span><span class="s1">'first'</span><span class="p">],</span> <span class="n">l</span><span class="p">[</span><span class="s1">'prev'</span><span class="p">]</span>
<span class="n">assert_equal</span> <span class="n">l</span><span class="p">[</span><span class="s1">'last'</span><span class="p">],</span> <span class="n">l</span><span class="p">[</span><span class="s1">'next'</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
<p>In this case I used <code>page</code> parameter and <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> doesn't have any rules what to use for the name of this parameter. However, those 4 generated links will use <code>page['number']</code> and <code>page['size']</code>, so we don't have much choice there…</p>
<p>Also, we can add extra <code>meta</code> object in <abbr title="JavaScript Object Notation">JSON</abbr> with more useful pagination data, like total number of pages or records:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">PostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="n">posts</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">page</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:page</span><span class="p">]</span> <span class="p">?</span> <span class="n">params</span><span class="p">[:</span><span class="n">page</span><span class="p">][</span><span class="ss">:number</span><span class="p">]</span> <span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">posts</span><span class="p">,</span> <span class="ss">meta: </span><span class="n">pagination_meta</span><span class="p">(</span><span class="n">posts</span><span class="p">)</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">pagination_meta</span><span class="p">(</span><span class="n">object</span><span class="p">)</span>
<span class="p">{</span>
<span class="ss">current_page: </span><span class="n">object</span><span class="p">.</span><span class="nf">current_page</span><span class="p">,</span>
<span class="ss">next_page: </span><span class="n">object</span><span class="p">.</span><span class="nf">next_page</span><span class="p">,</span>
<span class="ss">prev_page: </span><span class="n">object</span><span class="p">.</span><span class="nf">previous_page</span><span class="p">,</span>
<span class="ss">total_pages: </span><span class="n">object</span><span class="p">.</span><span class="nf">total_pages</span><span class="p">,</span>
<span class="ss">total_count: </span><span class="n">object</span><span class="p">.</span><span class="nf">total_entries</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>And we can add extra assert in the test:</p>
<pre class="highlight ruby"><code><span class="n">assert_equal</span> <span class="no">Post</span><span class="p">.</span><span class="nf">count</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'meta'</span><span class="p">][</span><span class="s1">'total-count'</span><span class="p">]</span>
</code></pre>
<h2 id="more-meta-key-goodies">More meta key goodies</h2>
<p>You can add even more information to your <abbr title="JavaScript Object Notation">JSON</abbr> messages using <code>meta</code> key. For example <abbr title="Application Programming Interface">API</abbr> version, last <abbr title="Application Programming Interface">API</abbr> update, copyright information, etc… For example, you can add following private method in ApplicationController:</p>
<pre class="highlight ruby"><code> <span class="k">def</span> <span class="nf">default_meta</span>
<span class="p">{</span>
<span class="ss">licence: </span><span class="s1">'CC-0'</span><span class="p">,</span>
<span class="ss">authors: </span><span class="p">[</span><span class="s1">'Saša'</span><span class="p">]</span>
<span class="p">}</span>
<span class="k">end</span>
</code></pre>
<p>Everywhere when you use <code>render json: object</code> you can change it to <code>render json: object, meta: default_meta</code>. When using pagination you can merge those hashes: <code>pagination_meta(posts).merge(default_meta)</code>.</p>
<h2 id="including-related-resources">Including related resources</h2>
<p>Sometimes you want to include related resources in <abbr title="JavaScript Object Notation">JSON</abbr> reply, that may be important. Por example, when requesting list of posts, we may need author's name to display it. Instead of making new request, we can include that data in <abbr title="JavaScript Object Notation">JSON</abbr> reply. We can do it easily when calling render method.</p>
<pre class="highlight ruby"><code><span class="n">render</span> <span class="ss">json: </span><span class="n">posts</span><span class="p">,</span> <span class="ss">meta: </span><span class="n">pagination_meta</span><span class="p">(</span><span class="n">posts</span><span class="p">),</span> <span class="ss">include: </span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span>
</code></pre>
<p>This will add <code>included</code> object in <abbr title="JavaScript Object Notation">JSON</abbr>, after <code>data</code> object.</p>
<h2 id="sorting">Sorting</h2>
<p>When implementing sorting with <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> we must use <code>sort</code> parameter. We can specify multiple fields for sorting, comma separated. Sorting is always in ascending order, unless you prefix sorting field with <code>-</code> character. For example <code>/posts?sort=-rating</code> will return Post records based on rating, highest first.</p>
<p>Lets write sorting test:</p>
<pre class="highlight ruby"><code> <span class="nb">test</span> <span class="s2">"Should get properly sorted list"</span> <span class="k">do</span>
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="s1">'rating DESC'</span><span class="p">).</span><span class="nf">first</span>
<span class="n">get</span> <span class="ss">:index</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">sort: </span><span class="s1">'-rating'</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">'attributes'</span><span class="p">][</span><span class="s1">'title'</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
<p>And implement it in controller:</p>
<pre class="highlight ruby"><code> <span class="k">def</span> <span class="nf">index</span>
<span class="n">posts</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">all</span>
<span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="s1">'sort'</span><span class="p">]</span>
<span class="n">f</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="s1">'sort'</span><span class="p">].</span><span class="nf">split</span><span class="p">(</span><span class="s1">','</span><span class="p">).</span><span class="nf">first</span>
<span class="n">field</span> <span class="o">=</span> <span class="n">f</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s1">'-'</span> <span class="p">?</span> <span class="n">f</span><span class="p">[</span><span class="mi">1</span><span class="p">.</span><span class="nf">.</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="p">:</span> <span class="n">f</span>
<span class="n">order</span> <span class="o">=</span> <span class="n">f</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s1">'-'</span> <span class="p">?</span> <span class="s1">'DESC'</span> <span class="p">:</span> <span class="s1">'ASC'</span>
<span class="k">if</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">has_attribute?</span><span class="p">(</span><span class="n">field</span><span class="p">)</span>
<span class="n">posts</span> <span class="o">=</span> <span class="n">posts</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">field</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">order</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">posts</span> <span class="o">=</span> <span class="n">posts</span><span class="p">.</span><span class="nf">page</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:page</span><span class="p">]</span> <span class="p">?</span> <span class="n">params</span><span class="p">[:</span><span class="n">page</span><span class="p">][</span><span class="ss">:number</span><span class="p">]</span> <span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">posts</span><span class="p">,</span> <span class="ss">meta: </span><span class="n">pagination_meta</span><span class="p">(</span><span class="n">posts</span><span class="p">),</span> <span class="ss">include: </span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
<h2 id="filtering">Filtering</h2>
<p><abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> suggests that we use <code>filter</code> parameter for filtering results, however doesn't care about filtering strategy used on server. We can implement it like we want. Lets build filtering based on category field.</p>
<p>New test (25 records with category <code>First</code> created in fixtures):</p>
<pre class="highlight ruby"><code> <span class="nb">test</span> <span class="s2">"Should get filtered list"</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:index</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">filter: </span><span class="s1">'First'</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="no">Post</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">category: </span><span class="s1">'First'</span><span class="p">).</span><span class="nf">count</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">].</span><span class="nf">length</span>
<span class="k">end</span>
</code></pre>
<p>And new <code>index</code> method in PostsController:</p>
<pre class="highlight ruby"><code> <span class="k">def</span> <span class="nf">index</span>
<span class="n">posts</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">all</span>
<span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:filter</span><span class="p">]</span>
<span class="n">posts</span> <span class="o">=</span> <span class="n">posts</span><span class="p">.</span><span class="nf">where</span><span class="p">([</span><span class="s2">"category = ?"</span><span class="p">,</span> <span class="n">params</span><span class="p">[</span><span class="ss">:filter</span><span class="p">]])</span>
<span class="k">end</span>
<span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="s1">'sort'</span><span class="p">]</span>
<span class="n">f</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="s1">'sort'</span><span class="p">].</span><span class="nf">split</span><span class="p">(</span><span class="s1">','</span><span class="p">).</span><span class="nf">first</span>
<span class="n">field</span> <span class="o">=</span> <span class="n">f</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s1">'-'</span> <span class="p">?</span> <span class="n">f</span><span class="p">[</span><span class="mi">1</span><span class="p">.</span><span class="nf">.</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="p">:</span> <span class="n">f</span>
<span class="n">order</span> <span class="o">=</span> <span class="n">f</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s1">'-'</span> <span class="p">?</span> <span class="s1">'DESC'</span> <span class="p">:</span> <span class="s1">'ASC'</span>
<span class="k">if</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">has_attribute?</span><span class="p">(</span><span class="n">field</span><span class="p">)</span>
<span class="n">posts</span> <span class="o">=</span> <span class="n">posts</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">field</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">order</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">posts</span> <span class="o">=</span> <span class="n">posts</span><span class="p">.</span><span class="nf">page</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:page</span><span class="p">]</span> <span class="p">?</span> <span class="n">params</span><span class="p">[:</span><span class="n">page</span><span class="p">][</span><span class="ss">:number</span><span class="p">]</span> <span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">posts</span><span class="p">,</span> <span class="ss">meta: </span><span class="n">pagination_meta</span><span class="p">(</span><span class="n">posts</span><span class="p">),</span> <span class="ss">include: </span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
<p>I kept this simple by implementing only filtering on one field.</p>
<h2 id="login-and-logout">Login and logout</h2>
<p>In all examples when user needs to update or create resource we use token from database as <code>X-Api-Key</code>. But how Client/<abbr title="Single Page Application">SPA</abbr> gets that token in the first place? Lets implement login and logout methods.</p>
<p><code>test/controllers/sessions_routes_test.rb</code>:</p>
<pre class="highlight ruby"><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">SessionsRoutesTest</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">TestCase</span>
<span class="nb">test</span> <span class="s2">"should route to create session"</span> <span class="k">do</span>
<span class="n">assert_routing</span><span class="p">({</span> <span class="ss">method: </span><span class="s1">'post'</span><span class="p">,</span> <span class="ss">path: </span><span class="s1">'/sessions'</span> <span class="p">},</span>
<span class="p">{</span> <span class="ss">controller: </span><span class="s2">"sessions"</span><span class="p">,</span> <span class="ss">action: </span><span class="s2">"create"</span> <span class="p">})</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should route to delete session"</span> <span class="k">do</span>
<span class="n">assert_routing</span><span class="p">({</span> <span class="ss">method: </span><span class="s1">'delete'</span><span class="p">,</span> <span class="ss">path: </span><span class="s1">'/sessions/something'</span><span class="p">},</span>
<span class="p">{</span> <span class="ss">controller: </span><span class="s2">"sessions"</span><span class="p">,</span> <span class="ss">action: </span><span class="s2">"destroy"</span><span class="p">,</span> <span class="ss">id: </span><span class="s2">"something"</span> <span class="p">})</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>In <code>config/routes.rb</code>:</p>
<pre class="highlight ruby"><code> <span class="n">post</span> <span class="s1">'sessions'</span> <span class="o">=></span> <span class="s1">'sessions#create'</span>
<span class="n">delete</span> <span class="s1">'sessions/:id'</span> <span class="o">=></span> <span class="s1">'sessions#destroy'</span>
</code></pre>
<p><code>app/serializers/session_serializer.rb</code>:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">SessionSerializer</span> <span class="o"><</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span>
<span class="n">attributes</span> <span class="ss">:id</span><span class="p">,</span> <span class="ss">:full_name</span><span class="p">,</span> <span class="ss">:token</span>
<span class="k">end</span>
</code></pre>
<p><code>test/controllers/sessions_controller_test.rb</code>:</p>
<pre class="highlight ruby"><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="nb">require</span> <span class="s1">'json'</span>
<span class="k">class</span> <span class="nc">SessionsControllerTest</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">TestCase</span>
<span class="nb">test</span> <span class="s2">"Creating new session with valid data should create new session"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_0'</span><span class="p">)</span>
<span class="vi">@request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'application/vnd.api+json'</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span>
<span class="ss">data: </span><span class="p">{</span>
<span class="ss">type: </span><span class="s1">'sessions'</span><span class="p">,</span>
<span class="ss">attributes: </span><span class="p">{</span>
<span class="ss">full_name: </span><span class="n">user</span><span class="p">.</span><span class="nf">full_name</span><span class="p">,</span>
<span class="ss">password: </span><span class="s1">'password'</span> <span class="p">}}}</span>
<span class="n">assert_response</span> <span class="mi">201</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">refute_equal</span> <span class="n">user</span><span class="p">.</span><span class="nf">token</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'attributes'</span><span class="p">][</span><span class="s1">'token'</span><span class="p">]</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"Should delete session"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_0'</span><span class="p">)</span>
<span class="n">delete</span> <span class="ss">:destroy</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">id: </span><span class="n">user</span><span class="p">.</span><span class="nf">token</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="mi">204</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>And <code>app/controllers/sessions_controller.rb</code>:</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">SessionsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="n">data</span> <span class="o">=</span> <span class="no">ActiveModelSerializers</span><span class="o">::</span><span class="no">Deserialization</span><span class="p">.</span><span class="nf">jsonapi_parse</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">error</span> <span class="n">params</span><span class="p">.</span><span class="nf">to_yaml</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">full_name: </span><span class="n">data</span><span class="p">[</span><span class="ss">:full_name</span><span class="p">]).</span><span class="nf">first</span>
<span class="n">head</span> <span class="mi">406</span> <span class="n">and</span> <span class="k">return</span> <span class="k">unless</span> <span class="n">user</span>
<span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="n">user</span><span class="p">.</span><span class="nf">regenerate_token</span>
<span class="n">render</span> <span class="ss">json: </span><span class="n">user</span><span class="p">,</span> <span class="ss">status: :created</span><span class="p">,</span> <span class="ss">meta: </span><span class="n">default_meta</span><span class="p">,</span>
<span class="ss">serializer: </span><span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span><span class="o">::</span><span class="no">SessionSerializer</span> <span class="n">and</span> <span class="k">return</span>
<span class="k">end</span>
<span class="n">head</span> <span class="mi">403</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">token: </span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">]).</span><span class="nf">first</span>
<span class="n">head</span> <span class="mi">404</span> <span class="n">and</span> <span class="k">return</span> <span class="k">unless</span> <span class="n">user</span>
<span class="n">user</span><span class="p">.</span><span class="nf">regenerate_token</span>
<span class="n">head</span> <span class="mi">204</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<ul>
<li>I created SessionSerializer in order to serialize and deserialize data needed for login.</li>
<li>To bring more security every time client requests login or logout I reset token. This regeneration is part of Rails 5.</li>
<li>When doing <code>render json: user</code>, client will get serialized <abbr title="JavaScript Object Notation">JSON</abbr> from UserSerializer. That is the reason that I explicitly use SessionSerializer.</li>
<li>There is also class named SessionSerializer in Rails and selecting that name was not a good thing. However, Session middleware is disabled in Rails for application created with <code>--api</code>.</li>
</ul>
<h2 id="token-validity">Token validity</h2>
<p>Let's add a bit more security with timeouts. If client does not access any <abbr title="Application Programming Interface">API</abbr> on server for 15 minutes, we'll not accept any future <abbr title="Application Programming Interface">API</abbr> calls from that client that change anything. Also, I'm going to use <code>meta</code> key to give information to client about logged-in status.</p>
<p>First thing is to change <code>updated_at</code> field for one user in fixture:</p>
<pre class="highlight ruby"><code><span class="o"><</span><span class="sx">% 6.times </span><span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="sx">%>
user_<%= i %></span><span class="p">:</span>
<span class="ss">full_name: </span><span class="o"><</span><span class="sx">%= "User Nr#{i}" %>
password_digest: <%=</span> <span class="no">BCrypt</span><span class="o">::</span><span class="no">Password</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="s1">'password'</span><span class="p">)</span> <span class="o">%></span>
<span class="ss">token: </span><span class="o"><</span><span class="sx">%= SecureRandom.base58(24) %>
updated_at: <%=</span> <span class="n">i</span> <span class="o">==</span> <span class="mi">5</span> <span class="p">?</span> <span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">.</span><span class="nf">ago</span> <span class="p">:</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span> <span class="sx">%>
<% end %></span>
</code></pre>
<p>Then we need change ApplicationController:</p>
<pre class="highlight ruby"><code> <span class="n">before_action</span> <span class="ss">:validate_login</span>
<span class="k">def</span> <span class="nf">validate_login</span>
<span class="n">token</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"X-Api-Key"</span><span class="p">]</span>
<span class="k">return</span> <span class="k">unless</span> <span class="n">token</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span> <span class="ss">token: </span><span class="n">token</span>
<span class="k">return</span> <span class="k">unless</span> <span class="n">user</span>
<span class="k">if</span> <span class="mi">15</span><span class="p">.</span><span class="nf">minutes</span><span class="p">.</span><span class="nf">ago</span> <span class="o"><</span> <span class="n">user</span><span class="p">.</span><span class="nf">updated_at</span>
<span class="n">user</span><span class="p">.</span><span class="nf">touch</span>
<span class="vi">@current_user</span> <span class="o">=</span> <span class="n">user</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">validate_user</span>
<span class="n">head</span> <span class="mi">403</span> <span class="n">and</span> <span class="k">return</span> <span class="k">unless</span> <span class="vi">@current_user</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">default_meta</span>
<span class="p">{</span>
<span class="ss">licence: </span><span class="s1">'CC-0'</span><span class="p">,</span>
<span class="ss">authors: </span><span class="p">[</span><span class="s1">'Saša'</span><span class="p">],</span>
<span class="ss">logged_in: </span><span class="p">(</span><span class="vi">@current_user</span> <span class="p">?</span> <span class="kp">true</span> <span class="p">:</span> <span class="kp">false</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">end</span>
</code></pre>
<ul>
<li>New <code>before_action</code> as called on every request and it checks of user have valid token and have access <abbr title="Application Programming Interface">API</abbr> server in last 15 minutes.</li>
<li>It also sets <code>@current_user</code> globally.</li>
<li>Old <code>validate_user</code> is changed a lot.</li>
<li><code>default_meta</code> gives information about client logged-in status.</li>
</ul>
<p>Here is integration test for this:</p>
<pre class="highlight ruby"><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="nb">require</span> <span class="s1">'json'</span>
<span class="k">class</span> <span class="nc">SessionFlowTestTest</span> <span class="o"><</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">IntegrationTest</span>
<span class="nb">test</span> <span class="s2">"login timeout and meta/logged-in key test"</span> <span class="k">do</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="s1">'user_5'</span><span class="p">)</span>
<span class="c1"># Not logged in, because of timeout</span>
<span class="n">get</span> <span class="s1">'/users'</span><span class="p">,</span> <span class="ss">params: </span><span class="kp">nil</span><span class="p">,</span>
<span class="ss">headers: </span><span class="p">{</span> <span class="s1">'X-Api-Key'</span> <span class="o">=></span> <span class="n">user</span><span class="p">.</span><span class="nf">token</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="kp">false</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'meta'</span><span class="p">][</span><span class="s1">'logged-in'</span><span class="p">]</span>
<span class="c1"># Log in</span>
<span class="n">post</span> <span class="s1">'/sessions'</span><span class="p">,</span>
<span class="ss">params: </span><span class="p">{</span>
<span class="ss">data: </span><span class="p">{</span>
<span class="ss">type: </span><span class="s1">'sessions'</span><span class="p">,</span>
<span class="ss">attributes: </span><span class="p">{</span>
<span class="ss">full_name: </span><span class="n">user</span><span class="p">.</span><span class="nf">full_name</span><span class="p">,</span>
<span class="ss">password: </span><span class="s1">'password'</span> <span class="p">}}}.</span><span class="nf">to_json</span><span class="p">,</span>
<span class="ss">headers: </span><span class="p">{</span> <span class="s1">'Content-Type'</span> <span class="o">=></span> <span class="s1">'application/vnd.api+json'</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="mi">201</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">token</span> <span class="o">=</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'data'</span><span class="p">][</span><span class="s1">'attributes'</span><span class="p">][</span><span class="s1">'token'</span><span class="p">]</span>
<span class="n">refute_equal</span> <span class="n">user</span><span class="p">.</span><span class="nf">token</span><span class="p">,</span> <span class="n">token</span>
<span class="c1"># Logged in</span>
<span class="n">get</span> <span class="s1">'/users'</span><span class="p">,</span> <span class="ss">params: </span><span class="kp">nil</span><span class="p">,</span>
<span class="ss">headers: </span><span class="p">{</span> <span class="s1">'X-Api-Key'</span> <span class="o">=></span> <span class="n">token</span> <span class="p">}</span>
<span class="n">assert_response</span> <span class="ss">:success</span>
<span class="n">jdata</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span>
<span class="n">assert_equal</span> <span class="kp">true</span><span class="p">,</span> <span class="n">jdata</span><span class="p">[</span><span class="s1">'meta'</span><span class="p">][</span><span class="s1">'logged-in'</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>Rails 5 <code>--api</code> option is just great. It really creates lightweight application and scaffolding works great (see notes below). It's a bit strange that when you use <code>--api</code> option, Rails will first create full application and then remove newly generated files that are not needed for <abbr title="Application Programming Interface">API</abbr> only application. Even more files get deleted if you use <code>-C</code> (without ActionCable).</p>
<p>Rails default uses their own simple <abbr title="JavaScript Object Notation">JSON</abbr> format. There is nothing wrong with that, but I prefer something more standard, with clearly defined message format, <abbr title="The Hypertext Transfer Protocol">HTTP</abbr> status codes, etc… <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> is currently the best option, and that is where 'active_model_serializers' gem shines.</p>
<p>Full source of this application <a href="https://github.com/Simplify/rails5_json_api_demo">is available on GitHub</a>.</p>
<h2 id="notes">Notes</h2>
<ul>
<li>Scaffolding works great with applications created with <code>--api</code>. I created everything by hand in this tutorial, method by method, because that way is easier to explain everything. Code generated with scaffolding is 90% done, you just need to change a little.</li>
<li>I used <code>X-Api-Key</code> header for authentication token. You can use <a href="http://docs.rubydocs.org/rails-5-0-0-rc1/classes/ActionController/HttpAuthentication/Token.html">ActionController::HttpAuthentication::Token</a> that is build in Rails or something else. I used simplest solution, just to illustrate how to secure some methods in controllers and how to call those methods from tests.</li>
<li>You may also want to use <abbr title="JSON Web Token">JWT</abbr>.</li>
<li>In this demo is no authorization implemented. Every logged in user can edit any other user or article. Normally, that is a problem, but this is just a demo and implementing that feature is more controller logic and have little to with this article. If somebody tries to edit resource that doesn't belong to him, you can just reply with <code>head 403</code>.</li>
<li>I didn't cover <a href="http://jsonapi.org/format/#crud-updating-relationships">updating relationships from <abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> spec</a>. It's not hard to do and often faster than updating whole record.</li>
<li>I didn't cover any framework/gem/tool for <abbr title="Application Programming Interface">API</abbr> documentation. I may come back to this point in the future articles.</li>
</ul>
<h2 id="related-links">Related links</h2>
<ul>
<li><a href="http://docs.rubydocs.org/rails-5-0-0-rc1/">Rails 5.0.0.rs1 <abbr title="Application Programming Interface">API</abbr> docs</a></li>
<li><a href="http://edgeguides.rubyonrails.org/api_app.html">Edge Rails <abbr title="Application Programming Interface">API</abbr> only guide</a> - More info about caching, rake middlewares and controller modules that may be useful to you</li>
<li><a href="http://jsonapi.org/"><abbr title="JavaScript Object Notation">JSON</abbr>:<abbr title="Application Programming Interface">API</abbr> specification</a> - Keep it open and consult it when in doubt</li>
<li><a href="https://github.com/rails-api/active_model_serializers/blob/master/docs/README.md">active_model_serializers documentation and guides</a></li>
</ul>
<h2 id="updates">Updates</h2>
<ul>
<li>2016-06-23: Updated for Rails 5.0.0.rc2</li>
<li>2016-07-03: Updated for Rails 5.0.0</li>
</ul>