{"id":10468,"date":"2016-02-01T12:11:09","date_gmt":"2016-02-01T10:11:09","guid":{"rendered":"http:\/\/www.webcodegeeks.com\/?p=10468"},"modified":"2016-01-22T16:30:21","modified_gmt":"2016-01-22T14:30:21","slug":"build-rails-apis-following-jsonapi-spec","status":"publish","type":"post","link":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/","title":{"rendered":"How to Build Rails APIs Following the json:api Spec"},"content":{"rendered":"<p>We\u2019ve talked before about <a href=\"http:\/\/www.webcodegeeks.com\/ruby\/building-json-api-rails-5\/\">how to build a JSON API with Rails 5<\/a>. We also discussed using Rails 5 in <code>--api<\/code> mode, serializing our JSON responses, caching, and also rate limiting\/throttling.<\/p>\n<p>These are all important topics, but even more important is producing a clear, standards-compliant API. We\u2019re going to look at how to build an API that conforms to the <a href=\"http:\/\/jsonapi.org\/\">json:api<\/a> spec.<\/p>\n<p>The json:api spec isn\u2019t just limited to how the server should format JSON responses. It also speaks to how the client should format requests, how to handle sorting, pagination, errors, and how new resources should be created. It even speaks to which media types and HTTP codes should be used.<\/p>\n<p>We\u2019re going to be implementing many of these things in our Rails app, and we\u2019ll also look at how we might test them. Lastly, we\u2019ll take a quick look at one possible authentication strategy and some tools we can use to produce beautiful documentation for our API.<\/p>\n<h2>json:api Is Big<\/h2>\n<p>The json:api spec is pretty big! It covers many of the possible features you may want to implement in a JSON API. <em>May<\/em> is the key word\u2026 you don\u2019t have to implement all of them for your API to be json:api compliant. For example: You don\u2019t have to have to implement sorting, but if you do, the json:api spec will tell you how it <em>must<\/em> be done.<\/p>\n<h3>Media types<\/h3>\n<p>One of the first things you\u2019ll notice with json:api is that it doesn\u2019t actually use the <code>application\/json<\/code> media type. The reason for this is that the group behind json:api actually went through the proper process and channels to register their own media type, which is <code>application\/vnd.api+json<\/code>. Part of the <a href=\"http:\/\/jsonapi.org\/format\/#content-negotiation-servers\">contract<\/a> of the server is that it will respond with the <code>Content-Type<\/code> header set properly to this media type.<\/p>\n<p>The easiest way I was able to find to get this working without having to set it manually in every action is to create an initializer to set them. I called it <code>register_json_mime_types.rb<\/code>, and it contains:<\/p>\n<pre class=\" brush:php\">api_mime_types = %W(\r\n  application\/vnd.api+json\r\n  text\/x-json\r\n  application\/json\r\n)\r\nMime::Type.register 'application\/vnd.api+json', :json, api_mime_types<\/pre>\n<p>Now whenever a request is made it will automatically have <code>Content-Type:application\/vnd.api+json; charset=utf-8<\/code> set in the response headers.<\/p>\n<h2>Creating a Resource<\/h2>\n<p>One of the questions that came up from my last article was how we should go about creating a resource. What format should the data come in? What URL should the data be sent to? How should the server respond on success? On errors? Thankfully all of those answers are available, and my goal is to show how to create a resource while following the json:api spec.<\/p>\n<p>For these examples, we\u2019ll be working with two models:<\/p>\n<ul>\n<li>User<\/li>\n<li>RentalUnit<\/li>\n<\/ul>\n<p>Their fields don\u2019t matter too much, and they have the relationship of a RentalUnit belonging to a User and a User having many RentalUnits.<\/p>\n<h3>Routing<\/h3>\n<p>Routing is probably one of the easiest parts. The specification follows RESTful routing pretty much identically to what Rails gives you by default when you define the route with <code>resources :rental_units<\/code>.<\/p>\n<p>The specific route we are interested in for creating a resource is <code>POST \/rental_units HTTP\/1.1<\/code>.<\/p>\n<h3>Format of data posted<\/h3>\n<p>If you\u2019ve done very much Rails development before, you\u2019re probably used to data being posted to the server in a fairly simple format, which may look somewhat like this:<\/p>\n<pre class=\" brush:php\">{\r\n  rental_unit: {\r\n    price_cents: 100000,\r\n    rooms: 2,\r\n    bathrooms: 1\r\n  }\r\n}<\/pre>\n<p>We\u2019ll have to change this a little bit to conform to the specification. In the guide we see that it must be formatted like this:<\/p>\n<pre class=\" brush:php\">data: {\r\n  type: 'rental_units',\r\n  attributes: {\r\n    price_cents: 100000,\r\n    rooms: 2,\r\n    bathrooms: 1\r\n  }\r\n}<\/pre>\n<p>This matches how json:api is formatted on response, so it is at least familiar\/consistent!<\/p>\n<p>To ensure that our API responds correctly, let\u2019s write a test. The test will post the data to the correct URL, and will then verify that the server responded with a <code>201<\/code> HTTP status code (which means resource created). After that, we\u2019ll look for a <code>Location<\/code> header, which tells us where we can find this new resource that was created.<\/p>\n<p>Lastly, we\u2019ll look to verify that it responded in the correct json:api response format using a custom matcher which I\u2019ll include below.<\/p>\n<pre class=\" brush:php\">require 'rails_helper'\r\n\r\nRSpec.describe \"Rental Units\", :type =&gt; :request do\r\n  let(:user) { create(:user) }\r\n\r\n  describe \"POST create\" do\r\n    it \"creates a rental unit\" do\r\n      post \"\/rental_units\", {\r\n        params: {\r\n          data: {\r\n            type: 'rental_units',\r\n            attributes: {\r\n              price_cents: 100000,\r\n              rooms: 2,\r\n              bathrooms: 1\r\n            }\r\n          }\r\n        },\r\n        headers: { 'X-Api-Key' =&gt; user.api_key }\r\n      }\r\n      expect(response.status).to eq(201)\r\n      expect(response.headers['Location']).to match(\/\\\/rental_units\\\/\\d$\/)\r\n      expect(response.body).to be_jsonapi_response_for('rental_units')\r\n    end\r\n  end\r\nend<\/pre>\n<p>Here is the custom rspec matcher I used. It does a simple check to make sure that the response conforms to the json:api spec when responding with a resource.<\/p>\n<pre class=\" brush:php\">RSpec::Matchers.define :be_jsonapi_response_for do |model|\r\n  match do |actual|\r\n    parsed_actual = JSON.parse(actual)\r\n    parsed_actual.dig('data', 'type') == model &amp;&amp;\r\n      parsed_actual.dig('data', 'attributes').is_a?(Hash) &amp;&amp;\r\n      parsed_actual.dig('data', 'relationships').is_a?(Hash)\r\n  end\r\nend<\/pre>\n<p>So if this is what our test looks like, what might the controller look like? It ends up looking fairly similar to how it might have before. The only difference here is that I have to dig a little deeper to get to the <code>attributes<\/code> coming in through the <code>params<\/code> object.<\/p>\n<pre class=\" brush:php\">class RentalUnitsController &lt; ApplicationController\r\n  def create\r\n    attributes = rental_unit_attributes.merge({user_id: auth_user.id})\r\n    @rental_unit = RentalUnit.new(attributes)\r\n\r\n    if @rental_unit.save\r\n      render json: @rental_unit, status: :created, location: @rental_unit\r\n    else\r\n      respond_with_errors(@rental_unit)\r\n    end\r\n  end\r\n\r\n  private\r\n\r\n  def rental_unit_params\r\n    params.require(:data).permit(:type, {\r\n      attributes: [:address, :rooms, :bathrooms, :price_cents]\r\n    })\r\n  end\r\n\r\n  def rental_unit_attributes\r\n    rental_unit_params[:attributes] || {}\r\n  end\r\nend<\/pre>\n<h2>When Errors Occur<\/h2>\n<p>You might have noticed above that I have a special method called <code>respond_with_errors<\/code> for when the <code>@rental_unit<\/code> object is unable to save. But before we get to that, let\u2019s take a look at how json:api expects us to format the errors:<\/p>\n<pre class=\" brush:php\">{\r\n  errors: [\r\n    status: 422,\r\n    source: {pointer: \"\/data\/attributes\/rooms\"},\r\n    detail: \"Must be present.\"\r\n  ]\r\n}<\/pre>\n<p>To help with this, I\u2019ve created a small method that lives in the <code>ApplicationController<\/code> to help respond with errors:<\/p>\n<pre class=\" brush:php\">def respond_with_errors(object)\r\n  render json: {errors: ErrorSerializer.serialize(object)}, status: :unprocessable_entity\r\nend<\/pre>\n<p>The <code>ErrorSerializer<\/code> class it is referring to is just a simple module which helps format the errors in the correct way.<\/p>\n<pre class=\" brush:php\">module ErrorSerializer\r\n  def self.serialize(object)\r\n    object.errors.messages.map do |field, errors|\r\n      errors.map do |error_message|\r\n        {\r\n          status: 422,\r\n          source: {pointer: \"\/data\/attributes\/#{field}\"},\r\n          detail: error_message\r\n        }\r\n      end\r\n    end.flatten\r\n  end\r\nend<\/pre>\n<p>I\u2019ve written a small test to make sure that the API responds correctly. It again uses a custom rspec matcher which I\u2019ll include below. What I am looking for here is that it responds with the <code>422<\/code> HTTP code (unprocessable entity) and that it contains an error for a specific field.<\/p>\n<pre class=\" brush:php\">it \"responds with errors\" do\r\n  post \"\/rental_units\", {\r\n    params: {\r\n      data: {\r\n        type: 'rental_units',\r\n        attributes: {}\r\n      }\r\n    },\r\n    headers: { 'X-Api-Key' =&gt; user.api_key }\r\n  }\r\n  expect(response.status).to eq(422)\r\n  expect(response.body).to have_jsonapi_errors_for('\/data\/attributes\/rooms')\r\nend<\/pre>\n<p>Here is the custom matcher which looks to see if there is an error for a specific field, in this case the <code>rooms<\/code> field:<\/p>\n<pre class=\" brush:php\">RSpec::Matchers.define :have_jsonapi_errors_for do |pointer|\r\n  match do |actual|\r\n    parsed_actual = JSON.parse(actual)\r\n    errors = parsed_actual['errors']\r\n    return false if errors.empty?\r\n    errors.any? do |error|\r\n      error.dig('source', 'pointer') == pointer\r\n    end\r\n  end\r\nend<\/pre>\n<h2>Sorting Results and Pagination<\/h2>\n<p>The next thing we are going to look at is how to sort results within our API. I\u2019ve grouped this together with pagination because the way I coded it they go hand in hand within the same class.<\/p>\n<p>You may be thinking at this point why I mentioned class. Doesn\u2019t this normally happen within the <code>index<\/code> action of the <code>RentalUnitsController<\/code>? Normally yes, but while doing this, I found that it was a bit more code than I was comfortable with to leave it all inside the controller. It\u2019s also a good example of how you might extract some complicated logic out of the controller into its own class or module.<\/p>\n<p>The action itself is dead simple:<\/p>\n<pre class=\" brush:php\">def index\r\n  rental_units_index = RentalUnitsIndex.new(self)\r\n  render json: rental_units_index.rental_units, links: rental_units_index.links\r\nend<\/pre>\n<p>The <code>RentalUnitsIndex<\/code> class has one job, to handle preparing the queries and data necessary to respond to the the <code>GET \/rental_units HTTP\/1.1<\/code> request. It receives <code>self<\/code> (the controller) so that it can access things such as the <code>params<\/code> object as well as the URL helpers.<\/p>\n<pre class=\" brush:php\">class RentalUnitsIndex\r\n\r\n  DEFAULT_SORTING = {created_at: :desc}\r\n  SORTABLE_FIELDS = [:rooms, :price_cents, :created_at]\r\n  PER_PAGE = 10\r\n\r\n  delegate :params, to: :controller\r\n  delegate :rental_units_url, to: :controller\r\n\r\n  attr_reader :controller\r\n\r\n  def initialize(controller)\r\n    @controller = controller\r\n  end\r\n\r\n  def rental_units\r\n    @rental_units ||= RentalUnit.includes(:user).\r\n      order(sort_params).\r\n      paginate(page: current_page, per_page: PER_PAGE)\r\n  end\r\n\r\n  def links\r\n    {\r\n      self:  rental_units_url(rebuild_params),\r\n      first: rental_units_url(rebuild_params.merge(first_page)),\r\n      prev:  rental_units_url(rebuild_params.merge(prev_page)),\r\n      next:  rental_units_url(rebuild_params.merge(next_page)),\r\n      last:  rental_units_url(rebuild_params.merge(last_page))\r\n    }\r\n  end\r\n\r\n  private\r\n\r\n    def current_page\r\n      (params.to_unsafe_h.dig('page', 'number') || 1).to_i\r\n    end\r\n\r\n    def first_page\r\n      {page: {number: 1}}\r\n    end\r\n\r\n    def next_page\r\n      {page: {number: [total_pages, current_page + 1].min}}\r\n    end\r\n\r\n    def prev_page\r\n      {page: {number: [1, current_page - 1].max}}\r\n    end\r\n\r\n    def last_page\r\n      {page: {number: total_pages}}\r\n    end\r\n\r\n    def total_pages\r\n      @total_pages ||= rental_units.total_pages\r\n    end\r\n\r\n    def sort_params\r\n      SortParams.sorted_fields(params[:sort], SORTABLE_FIELDS, DEFAULT_SORTING)\r\n    end\r\n\r\n    def rebuild_params\r\n      @rebuild_params ||= begin\r\n        rejected = ['action', 'controller']\r\n        params.to_unsafe_h.reject { |key, value| rejected.include?(key.to_s) }\r\n      end\r\n    end\r\n\r\nend<\/pre>\n<p>The nice thing about the way this is written is that it would be quite easy to test what might end up being complicated logic to perform sorting, pagination, and maybe at some point in the future, filtering of the rental units.<\/p>\n<p>If this were an API I was developing for real, I would most likely extract a lot of these generic methods into a parent class.<\/p>\n<h3>Sorting with json:api<\/h3>\n<p>Sorting in json:api is done through a single query param called <code>sort<\/code> which comes through the URL. It might look like this <code>?sort=-rooms,price_cents<\/code>, which would sort descending by the <code>rooms<\/code> field, and then ascending by the <code>price_cents<\/code> field.<\/p>\n<p>This functionality is handled by the <code>sort_params<\/code> method, which farms out the work to a module called <code>SortParams<\/code>. This module has the job of taking a string such as <code>-rooms,price_cents<\/code> and converting it into the usual <code>Hash<\/code> that the <code>order<\/code> method wants to receive. From <code>-rooms,price_cents<\/code> to <code>{rooms: :desc, price_cents: :asc}<\/code>.<\/p>\n<pre class=\" brush:php\">module SortParams\r\n  def self.sorted_fields(sort, allowed, default)\r\n    allowed = allowed.map(&amp;:to_s)\r\n    fields = sort.to_s.split(',')\r\n\r\n    ordered_fields = convert_to_ordered_hash(fields)\r\n    filtered_fields = ordered_fields.select { |key, value| allowed.include?(key) }\r\n\r\n    filtered_fields.present? ? filtered_fields : default\r\n  end\r\n\r\n  def self.convert_to_ordered_hash(fields)\r\n    fields.each_with_object({}) do |field, hash|\r\n      if field.start_with?('-')\r\n        field = field[1..-1]\r\n        hash[field] = :desc\r\n      else\r\n        hash[field] = :asc\r\n      end\r\n    end\r\n  end\r\nend<\/pre>\n<p>We\u2019ve been trying to test functionality as we build it, so here are the tests to ensure that it is sorting correctly when the <code>sort<\/code> param is included.<\/p>\n<p>We\u2019ve created some rental units and are requesting that they return in descending order based on the amount of rooms they have.<\/p>\n<pre class=\" brush:php\">describe \"GET index\" do\r\n  it \"returns sorted results\" do\r\n    create(:rental_unit, rooms: 4)\r\n    create(:rental_unit, rooms: 5)\r\n    create(:rental_unit, rooms: 3)\r\n\r\n    get \"\/rental_units\", {\r\n      params: { sort: '-rooms' },\r\n      headers: { 'X-Api-Key' =&gt; user.api_key }\r\n    }\r\n\r\n    expect(response.status).to eq(200)\r\n    parsed_body = JSON.parse(response.body)\r\n    rental_unit_ids = parsed_body['data'].map{ |unit| unit['attributes']['rooms'].to_i }\r\n    expect(rental_unit_ids).to eq([5,4,3])\r\n  end\r\nend<\/pre>\n<h3>Pagination with json:api<\/h3>\n<p>With pagination, most of the details are left up to the programmer to decide. That is because pagination could be done quite differently from app to app, either page by page or by where your cursor is on the screen (say in an infinite scroll view). What <em>is<\/em> dictated by the spec is that you should pass this information through query params in the <code>page<\/code> key.<\/p>\n<p>It may end up looking like this: <code>?page[number]=1<\/code>. You are also required to include a <code>links<\/code> key in the response which provides links to the current page (self), the first, previous, next, and last links. Most of these details are handled by the <code>RentalUnitsIndex<\/code> class in combination with the <a href=\"https:\/\/github.com\/mislav\/will_paginate\">will_paginate<\/a> gem.<\/p>\n<p>These are included in the rendered response from within the controller which calls out to our helper class:<\/p>\n<pre class=\" brush:php\">render json: rental_units_index.rental_units, links: rental_units_index.links<\/pre>\n<p>Here we will test to ensure that it is returning paginated results along with the links correctly.<\/p>\n<pre class=\" brush:php\">it \"returns paginated results\" do\r\n  (RentalUnitsIndex::PER_PAGE + 1).times { create(:rental_unit) }\r\n\r\n  get \"\/rental_units\", {\r\n    params: { sort: '-rooms', page: {number: 2} },\r\n    headers: { 'X-Api-Key' =&gt; user.api_key }\r\n  }\r\n\r\n  expect(response.status).to eq(200)\r\n\r\n  parsed_body = JSON.parse(response.body)\r\n  expect(parsed_body['data'].size).to eq(1)\r\n  expect(URI.unescape(parsed_body['links']['first'])).to eq('http:\/\/www.example.com\/rental_units?page[number]=1&amp;sort=-rooms')\r\n  expect(URI.unescape(parsed_body['links']['prev'])).to eq('http:\/\/www.example.com\/rental_units?page[number]=1&amp;sort=-rooms')\r\n  expect(URI.unescape(parsed_body['links']['next'])).to eq('http:\/\/www.example.com\/rental_units?page[number]=2&amp;sort=-rooms')\r\n  expect(URI.unescape(parsed_body['links']['last'])).to eq('http:\/\/www.example.com\/rental_units?page[number]=2&amp;sort=-rooms')\r\nend<\/pre>\n<p>One interesting but slightly unrelated thing I discovered while working on this article was that <code>params<\/code> has changed in Rails 5. It is <a href=\"http:\/\/eileencodes.com\/posts\/actioncontroller-parameters-now-returns-an-object-instead-of-a-hash\/\">now a different object<\/a> instead of the old <code>HashWithIndifferentAccess<\/code> object. Thanks to <a href=\"https:\/\/twitter.com\/eileencodes\">Eileen Uchitelle<\/a> for a great article pointing out what has changed.<\/p>\n<h2>Authentication<\/h2>\n<p>The type of authentication you need depends on the type of application you are building. If it is a public API, you might not need it at all, but more and more I have seen a simple API key given out, allowing the server to track your usage and giving them more control over its access.<\/p>\n<p>Alternatively, it may be an API which powers a single page application or a mobile app. Here you will still go with a token solution, perhaps like the one that <a href=\"https:\/\/github.com\/lynndylanhurley\/devise_token_auth\">devise_token_auth<\/a> gives you, but it is a little bit more complicated to implement because there is more security involved (the token may change on every single request).<\/p>\n<p>The main thing to take away is that there are no cookies involved\u2026 there is no magical state that the browser handles for us. In fact, because we\u2019ve built this app using the <code>--api<\/code> version of Rails 5, the cookies functionality doesn\u2019t even exist.<\/p>\n<p>We\u2019re going to go with the first solution I mentioned, a simple API key that is attached to the user\u2019s account. When they request a page, they\u2019ll include that in the request headers under the <code>X-Api-Key<\/code> value.<\/p>\n<p>Here is what our <code>User<\/code> model looks like. It will assign each new user a key (which could later be regenerated and\/or modified if we need to shut this account out because of abuse).<\/p>\n<pre class=\" brush:php\">class User &lt; ActiveRecord::Base\r\n  has_many :rental_units\r\n\r\n  before_create :set_initial_api_key\r\n\r\n  private\r\n\r\n  def set_initial_api_key\r\n    self.api_key ||= generate_api_key\r\n  end\r\n\r\n  def generate_api_key\r\n    SecureRandom.uuid\r\n  end\r\nend<\/pre>\n<p>Inside of the <code>ApplicationController<\/code>, we\u2019ll include a few small methods which will help us validate that the token is in fact correct, who it belongs to, and to respond with a <code>401<\/code> unauthorized response if it is invalid.<\/p>\n<pre class=\" brush:php\">class ApplicationController &lt; ActionController::API\r\n  before_action :authenticate\r\n\r\n  private\r\n\r\n    def authenticate\r\n      authenticate_api_key || render_unauthorized\r\n    end\r\n\r\n    def authenticate_api_key\r\n      api_key = request.headers['X-Api-Key']\r\n      @auth_user = User.find_by(api_key: api_key)\r\n    end\r\n\r\n    def auth_user\r\n      @auth_user\r\n    end\r\n\r\n    def render_unauthorized\r\n      render json: 'Bad credentials', status: 401\r\n    end\r\nend<\/pre>\n<p>Here we have a test that ensures it is working correctly:<\/p>\n<pre class=\" brush:php\">it \"responds with unauthorized\" do\r\n  post \"\/rental_units\", {\r\n    params: {},\r\n    headers: { 'X-Api-Key' =&gt; '12345' }\r\n  }\r\n  expect(response.status).to eq(401)\r\nend<\/pre>\n<p>As far as I can tell, json:api doesn\u2019t speak much or at all about authentication, and it is left up to the implementor to decide what works best for their specific API and its use case.<\/p>\n<h2>Documenting Your API<\/h2>\n<p>Part of what makes APIs great \u2014 such as the ones from <a href=\"https:\/\/stripe.com\/docs\/api\">Stripe<\/a>, <a href=\"https:\/\/www.twilio.com\/docs\/api\/rest\">Twilio<\/a>, or <a href=\"https:\/\/developer.github.com\/v3\/\">GitHub<\/a> \u2014 is how good their documentation is. I won\u2019t go into too much detail here other than to recommend two of the best approaches I\u2019ve seen.<\/p>\n<p>The first approach, a gem called <a href=\"https:\/\/github.com\/Apipie\/apipie-rails\">apipie-rails<\/a>, is done by adding extra details to your tests, which automatically generates documentation. This has the benefit of being easy to keep in sync with your code as it is right there alongside it, but I find that it can tend to clutter up the tests a little bit.<\/p>\n<p>The second approach is one called <a href=\"https:\/\/github.com\/tripit\/slate\">slate<\/a> which allows you to create some really beautiful API documentation, much like the ones I listed above. This is done in markdown, and it ends up generating static HTML\/CSS files that you can host.<\/p>\n<h2>Conclusion<\/h2>\n<p>In this article, we expanded upon a previous article about <a href=\"http:\/\/www.webcodegeeks.com\/ruby\/building-json-api-rails-5\/\">creating JSON APIs within Rails<\/a>. We tackled some common problems that occur when creating APIs, such as wanting pagination, sorting, errors, and authentication. We looked at how we can develop these features in keeping with the json:api spec.<\/p>\n<p>By doing so, we avoid needing to have conversations about how to implement each feature, and we can instead focus on the specifics of our application.<\/p>\n<div class=\"attribution\">\n<table>\n<tbody>\n<tr>\n<td><span class=\"reference\">Reference: <\/span><\/td>\n<td><a href=\"http:\/\/blog.codeship.com\/the-json-api-spec\/\">How to Build Rails APIs Following the json:api Spec<\/a> from our <a href=\"http:\/\/www.webcodegeeks.com\/join-us\/wcg\/\">WCG partner<\/a> Florian Motlik at the <a href=\"http:\/\/blog.codeship.com\/\">Codeship Blog<\/a> blog.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>We\u2019ve talked before about how to build a JSON API with Rails 5. We also discussed using Rails 5 in &#8211;api mode, serializing our JSON responses, caching, and also rate limiting\/throttling. These are all important topics, but even more important is producing a clear, standards-compliant API. We\u2019re going to look at how to build an &hellip;<\/p>\n","protected":false},"author":113,"featured_media":921,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[21],"tags":[40,95],"class_list":["post-10468","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ruby","tag-json","tag-rails"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.5 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>How to Build Rails APIs Following the json:api Spec - Web Code Geeks - 2026<\/title>\n<meta name=\"description\" content=\"We\u2019ve talked before about how to build a JSON API with Rails 5. We also discussed using Rails 5 in --api mode, serializing our JSON responses, caching,\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How to Build Rails APIs Following the json:api Spec - Web Code Geeks - 2026\" \/>\n<meta property=\"og:description\" content=\"We\u2019ve talked before about how to build a JSON API with Rails 5. We also discussed using Rails 5 in --api mode, serializing our JSON responses, caching,\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/\" \/>\n<meta property=\"og:site_name\" content=\"Web Code Geeks\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/webcodegeeks\" \/>\n<meta property=\"article:published_time\" content=\"2016-02-01T10:11:09+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"150\" \/>\n\t<meta property=\"og:image:height\" content=\"150\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Leigh Halliday\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@leighchalliday\" \/>\n<meta name=\"twitter:site\" content=\"@webcodegeeks\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Leigh Halliday\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"15 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/\"},\"author\":{\"name\":\"Leigh Halliday\",\"@id\":\"https:\/\/www.webcodegeeks.com\/#\/schema\/person\/e496b17f78cdca27723b8e225dc6ab6b\"},\"headline\":\"How to Build Rails APIs Following the json:api Spec\",\"datePublished\":\"2016-02-01T10:11:09+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/\"},\"wordCount\":2035,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg\",\"keywords\":[\"JSON\",\"Rails\"],\"articleSection\":[\"Ruby\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/\",\"url\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/\",\"name\":\"How to Build Rails APIs Following the json:api Spec - Web Code Geeks - 2026\",\"isPartOf\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg\",\"datePublished\":\"2016-02-01T10:11:09+00:00\",\"description\":\"We\u2019ve talked before about how to build a JSON API with Rails 5. We also discussed using Rails 5 in --api mode, serializing our JSON responses, caching,\",\"breadcrumb\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#primaryimage\",\"url\":\"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg\",\"contentUrl\":\"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg\",\"width\":150,\"height\":150},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.webcodegeeks.com\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Ruby\",\"item\":\"https:\/\/www.webcodegeeks.com\/category\/ruby\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"How to Build Rails APIs Following the json:api Spec\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.webcodegeeks.com\/#website\",\"url\":\"https:\/\/www.webcodegeeks.com\/\",\"name\":\"Web Code Geeks\",\"description\":\"Web Developers Resource Center\",\"publisher\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.webcodegeeks.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.webcodegeeks.com\/#organization\",\"name\":\"Exelixis Media P.C.\",\"url\":\"https:\/\/www.webcodegeeks.com\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.webcodegeeks.com\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2022\/06\/exelixis-logo.png\",\"contentUrl\":\"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2022\/06\/exelixis-logo.png\",\"width\":864,\"height\":246,\"caption\":\"Exelixis Media P.C.\"},\"image\":{\"@id\":\"https:\/\/www.webcodegeeks.com\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/webcodegeeks\",\"https:\/\/x.com\/webcodegeeks\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.webcodegeeks.com\/#\/schema\/person\/e496b17f78cdca27723b8e225dc6ab6b\",\"name\":\"Leigh Halliday\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.webcodegeeks.com\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/bd40251a1acc424c292c35a3485264a801efa20efa7063c3e320a0a354ddafac?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/bd40251a1acc424c292c35a3485264a801efa20efa7063c3e320a0a354ddafac?s=96&d=mm&r=g\",\"caption\":\"Leigh Halliday\"},\"description\":\"Leigh is a developer at theScore. He writes about Ruby, Rails, and software development on his personal site.\",\"sameAs\":[\"http:\/\/www.leighhalliday.com\/\",\"https:\/\/x.com\/leighchalliday\"],\"url\":\"https:\/\/www.webcodegeeks.com\/author\/leigh-halliday\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How to Build Rails APIs Following the json:api Spec - Web Code Geeks - 2026","description":"We\u2019ve talked before about how to build a JSON API with Rails 5. We also discussed using Rails 5 in --api mode, serializing our JSON responses, caching,","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/","og_locale":"en_US","og_type":"article","og_title":"How to Build Rails APIs Following the json:api Spec - Web Code Geeks - 2026","og_description":"We\u2019ve talked before about how to build a JSON API with Rails 5. We also discussed using Rails 5 in --api mode, serializing our JSON responses, caching,","og_url":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/","og_site_name":"Web Code Geeks","article_publisher":"https:\/\/www.facebook.com\/webcodegeeks","article_published_time":"2016-02-01T10:11:09+00:00","og_image":[{"width":150,"height":150,"url":"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg","type":"image\/jpeg"}],"author":"Leigh Halliday","twitter_card":"summary_large_image","twitter_creator":"@leighchalliday","twitter_site":"@webcodegeeks","twitter_misc":{"Written by":"Leigh Halliday","Est. reading time":"15 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#article","isPartOf":{"@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/"},"author":{"name":"Leigh Halliday","@id":"https:\/\/www.webcodegeeks.com\/#\/schema\/person\/e496b17f78cdca27723b8e225dc6ab6b"},"headline":"How to Build Rails APIs Following the json:api Spec","datePublished":"2016-02-01T10:11:09+00:00","mainEntityOfPage":{"@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/"},"wordCount":2035,"commentCount":0,"publisher":{"@id":"https:\/\/www.webcodegeeks.com\/#organization"},"image":{"@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#primaryimage"},"thumbnailUrl":"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg","keywords":["JSON","Rails"],"articleSection":["Ruby"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/","url":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/","name":"How to Build Rails APIs Following the json:api Spec - Web Code Geeks - 2026","isPartOf":{"@id":"https:\/\/www.webcodegeeks.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#primaryimage"},"image":{"@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#primaryimage"},"thumbnailUrl":"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg","datePublished":"2016-02-01T10:11:09+00:00","description":"We\u2019ve talked before about how to build a JSON API with Rails 5. We also discussed using Rails 5 in --api mode, serializing our JSON responses, caching,","breadcrumb":{"@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#primaryimage","url":"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg","contentUrl":"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2014\/10\/json-logo.jpg","width":150,"height":150},{"@type":"BreadcrumbList","@id":"https:\/\/www.webcodegeeks.com\/ruby\/build-rails-apis-following-jsonapi-spec\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.webcodegeeks.com\/"},{"@type":"ListItem","position":2,"name":"Ruby","item":"https:\/\/www.webcodegeeks.com\/category\/ruby\/"},{"@type":"ListItem","position":3,"name":"How to Build Rails APIs Following the json:api Spec"}]},{"@type":"WebSite","@id":"https:\/\/www.webcodegeeks.com\/#website","url":"https:\/\/www.webcodegeeks.com\/","name":"Web Code Geeks","description":"Web Developers Resource Center","publisher":{"@id":"https:\/\/www.webcodegeeks.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.webcodegeeks.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.webcodegeeks.com\/#organization","name":"Exelixis Media P.C.","url":"https:\/\/www.webcodegeeks.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.webcodegeeks.com\/#\/schema\/logo\/image\/","url":"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2022\/06\/exelixis-logo.png","contentUrl":"https:\/\/www.webcodegeeks.com\/wp-content\/uploads\/2022\/06\/exelixis-logo.png","width":864,"height":246,"caption":"Exelixis Media P.C."},"image":{"@id":"https:\/\/www.webcodegeeks.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/webcodegeeks","https:\/\/x.com\/webcodegeeks"]},{"@type":"Person","@id":"https:\/\/www.webcodegeeks.com\/#\/schema\/person\/e496b17f78cdca27723b8e225dc6ab6b","name":"Leigh Halliday","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.webcodegeeks.com\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/bd40251a1acc424c292c35a3485264a801efa20efa7063c3e320a0a354ddafac?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/bd40251a1acc424c292c35a3485264a801efa20efa7063c3e320a0a354ddafac?s=96&d=mm&r=g","caption":"Leigh Halliday"},"description":"Leigh is a developer at theScore. He writes about Ruby, Rails, and software development on his personal site.","sameAs":["http:\/\/www.leighhalliday.com\/","https:\/\/x.com\/leighchalliday"],"url":"https:\/\/www.webcodegeeks.com\/author\/leigh-halliday\/"}]}},"_links":{"self":[{"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/posts\/10468","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/users\/113"}],"replies":[{"embeddable":true,"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/comments?post=10468"}],"version-history":[{"count":0,"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/posts\/10468\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/media\/921"}],"wp:attachment":[{"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/media?parent=10468"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/categories?post=10468"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.webcodegeeks.com\/wp-json\/wp\/v2\/tags?post=10468"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}