This tutorial is out of date and no longer maintained.
Note: Part one of a three-part series.
Build a RESTful JSON API With Rails 5 - Part One Build a RESTful JSON API With Rails 5 - Part Two Build a RESTful JSON API With Rails 5 - Part Three
In part two of this tutorial, we added token-based authentication with JWT (JSON Web Tokens) to our todo API.
In this final part of the series, we’ll wrap with the following:
When building an API whether public or internal facing, it’s highly recommended that you version it. This might seem trivial when you have total control over all clients. However, when the API is public-facing, you want to establish a contract with your clients. Every breaking change should be a new version. Convincing enough? Great, let’s do this!
In order to version a Rails API, we need to do two things:
Rails routing supports advanced constraints. Provided an object that responds to matches?
, you can control which controller handles a specific route.
We’ll define a class ApiVersion
that checks the API version from the request headers and routes to the appropriate controller module. The class will live in app/lib
since it’s non-domain-specific.
Implement ApiVersion
The ApiVersion
class accepts a version and a default flag on initialization. In accordance with Rails constraints, we implement an instance method matches?
. This method will be called with the request object upon initialization.
From the request object, we can access the Accept
headers and check for the requested version or if the instance is the default version. This process is called content negotiation. Let’s add some more context to this.
REST is closely tied to the HTTP specification. HTTP defines mechanisms that make it possible to serve different versions (representations) of a resource at the same URI. This is called content negotiation.
Our ApiVersion
class implements server-driven content negotiation where the client (user agent) informs the server what media types it understands by providing an Accept HTTP header.
According to the Media Type Specification, you can define your own media types using the vendor tree i.e., application/vnd.example.resource+json
.
The vendor tree is used for media types associated with publicly available products. It uses the “vnd” facet.
Thus, we define a custom vendor media type application/vnd.todos.{version_number}+json
giving clients the ability to choose which API version they require.
Cool, now that we have the constraint class, let’s change our routing to accommodate this.
Since we don’t want to have the version number as part of the URI (this is argued as an anti-pattern), we’ll make use of the module scope to namespace our controllers.
Let’s move the existing todos and todo-items resources into a v1
namespace.
We’ve set the version constraint at the namespace level. Thus, this will be applied to all resources within it. We’ve also defined v1
as the default version; in cases where the version is not provided, the API will default to v1
.
In the event we were to add new versions, they would have to be defined above the default version since Rails will cycle through all routes from top to bottom searching for one that matches
(till method matches?
resolves to true).
Next up, let’s move the existing todos and items controllers into the v1
namespace. First, create a module directory in the controllers folder.
Move the files into the module folder.
That’s not all, let’s define the controllers in the v1 namespace. Let’s start with the todos controller.
Do the same for the items controller.
Let’s fire up the server and run some tests.
In case we attempt to access a nonexistent version, the API will default to v1
since we set it as the default version. For testing purposes, let’s define v2
.
Generate a v2
todos controller
Define the namespace in the routes.
Remember, non-default versions have to be defined above the default version.
Since this is test controller, we’ll define an index controller with a dummy response.
Note the namespace syntax, this is shorthand in Ruby to define a class within a namespace. Great, now fire up the server once more and run some tests.
Voila! Our API responds to version 2!
At this point, if we wanted to get a todo and its items, we’d have to make two API calls. Although this works well, it’s not ideal.
We can achieve this with serializers. Serializers allow for custom representations of JSON responses. Active model serializers make it easy to define which model attributes and relationships need to be serialized. In order to get todos with their respective items, we need to define serializers on the Todo model to include its attributes and relationships.
First, let’s add active model serializers to the Gemfile:
Run bundle to install it:
Generate a serializer from the todo model:
This creates a new directory app/serializers
and adds a new file todo_serializer.rb
. Let’s define the todo serializer with the data that we want it to contain.
We define a whitelist of attributes to be serialized and the model association (only defined attributes will be serialized). We’ve also defined a model association to the item model, this way the payload will include an array of items. Fire up the server, let’s test this.
This is great. One request to rule them all!
Our todos API has suddenly become very popular. All of a sudden everyone has something to do. Our data set has grown substantially. To make sure the requests are still fast and optimized, we’re going to add pagination; we’ll give clients the power to say what portion of data they require.
To achieve this, we’ll make use of the will_paginate gem.
Let’s add it to the Gemfile:
Install it:
Let’s modify the todos controller index action to paginate its response.
The index action checks for the page number in the request params. If provided, it’ll return the page data with each page having twenty records each. As always, let’s fire up the Rails server and run some tests.
The page number is part of the query string. Note that when we request the second page, we get an empty array. This is because we don’t have more than 20 records in the database.
Let’s seed some test data into the database.
Add faker and install faker gem. Faker generates data at random.
In db/seeds.rb
let’s define seed data.
Seed the database by running:
Awesome, fire up the server and rerun the HTTP requests. Since we have test data, we’re able to see data from different pages.
Congratulations for making it this far! We’ve come a long way! We’ve gone through generating an API-only Rails application, setting up a test framework, using TDD to implement the todo API, adding token-based authentication with JWT, versioning our API, serializing with active model serializers, and adding pagination features.
Having gone through this series, I believe you should be able to build a RESTful API with Rails 5. Feel free to leave any feedback you may have in the comments section below. If you found the tutorial helpful, don’t hesitate to hit that share button. Cheers!
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!