Rails API with Active Model Serializers – Part 1

In this tutorial, we’ll build an API, which helps to organize the workflow of a library. It allows us to borrow a book, give it back, or to create a user, a book or an author. What’s more, the administration part will be available only for admins – CRUD of books, authors, and users. Authentication will be handled via HTTP Tokens.

To build this, I’m using Rails API 5 with ActiveModelSerializers. In the next part of this series, I’ll show how to fully test API, which we’re gonna build.

ActiveModelSerializers is a library which helps to build an object, which is returned to create a JSON object. In this case, we don’t need to use any views, we just return a serializer which represents an object. It’s fully customizable and reusable – which is great!

API Versioning

One of the most important things, when you build new API, is versioning. You should remember to add a namespace (v1, v2) to your API. Why? The next version of your API will probably be different.

The problematic part is compatibility. Some of your customers will probably use an old version of your product. In this case, you can keep the old product under a v1 namespace while building a new one under a v2 namespace. For example:

In this case, all versions of the application will be supported – your customers will be happy!

Serializers

As I told before, we’ll use serializers to build JSONs. What’s cool about them is that they’re objects – they can be used in every part of the application. Views are not needed! What’s more, you can include or exclude any field you want!

As you can see, we also define attributes; but what’s new is the overidden author method. In some cases, we’ll need a serialized author object and in some cases not. We can specify in the options which object we need (second parameter – options = {}). Why do we need it?

Check this case – we’ll create a book object. In this object we also include an author that includes books. Each book is serialized so it also will return an author. We would get an infinite loop – that’s why we need to specify if a serialized object is needed. What’s more, we can create a serializer, for each action (index, update etc.)

As you can see, these return a basic object, for example: render Author.find(1). How does the application know that we want to render a serializer? By adding an adapter: :json options. From now on, by default the serializers will be used to render JSONs. You can read more about it here, in the official documentation.

Our application needs some fake data, let’s add it by filling our seeds files and using the Faker gem:

Rack-Cors

I mentioned previously that we’ll use Rack-CORS – an awesome tool that helps make cross-origin AJAX calls. Well, adding it to the Gemfile is not enough – we need to set up it in the application.rb file, too.

Now we need to tell our application that we want to use rack-attack. Add it to the application.rb:

config.middleware.use Rack::Attack

To add a filtering functionality, we need to add a new initializer to the config/initializers directory. Let’s call it rack_attack.rb:

class Rack::Attack
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
Rack::Attack.throttle('req/ip', limit: 5, period: 1.second) do |req|
req.ip
end
Rack::Attack.throttled_response = lambda do |env|
# Using 503 because it may make attacker think that they have successfully
# DOSed the site. Rack::Attack returns 429 for throttling by default
[ 503, {}, ["Server Errorn"]]
end
end

What did we add here? Basically, we’re now allow to make 5 requests per IP address per 1 second. If someone hits one of our endpoints more that 5 times in 1 second, we return 503 HTTP status as a response with the Server Error message.

Tokens – API keys

Like I mentioned at the beginning, we secure our API with HTTP Tokens. Each user has a unique token. Through this token we find a user in our database and set it as current_user.

Normal users are able only to borrow or return a book. What’s more, if a requested book is not borrowed by us, we can’t return it. Also, we can’t borrow an already borrowed book. Let’s add a new field to the users table:

The way things are now, we won’t secure our API. We don’t check to see if a request contains an API key. We need to change it. To use the authenticate_with_http_token method we need to include ActionController::HttpAuthentication::Token::ControllerMethods module.

Once we’re done with it, let’s write some code. We need to authorize each request – if a user/admin is found with a requested token, we set it in an instance variable for further purposes. If a token is not provided, we should return a JSON with 401 HTTP status code.

Furthermore, it’s best practices to rescue from a not found record – for example, if someone requests a non-existent book. We should handle it and return a valid HTTP status code, rather than throwing an application error. Please add the following code to the ApplicationController:

Pundit

For now, we can check if someone’s request includes a Token, but we can’t check if someone can update/create a record (is an admin in fact). To do it we will add some filters and Pundit.

Pundit will be used for checking if the person returning a book is the person who borrowed it. To be honest, using Pundit for only one action is not necessary. But I want to show how to customize it and add more info to Pundit’s scope. I think it could be useful for you.

First of all, we need to include Pundit and add a method which rescues from a 403 error. Also, we will add the authorize method, which checks if a request is from a user or an admin.

Another important piece is a method which sets current_user as an admin’s request. For example, if an admin wants to modify info about a borrowed book by adding a user. In this case, we need to pass the user_id parameter and set current_user in an instance variable – like in a request from an ordinary user.

As you can see it’s an ordinary, plain ruby class which sets a user and an admin.

Pundit by default generates the ApplicationPolicy class. But the main problem is that by default, it only includes a record and a user in a context. How can we deal with a situation where we want to keep an admin and a user?

Adding a user_context would be a good idea! In this case, we store the whole instance of the UserContext class and we can also set a user and an admin in our policy classes:

In the return_book? method we check if a user is an admin or a user who borrowed a book. What’s more, Pundit adds a method called policy_scope that returns all records, which should be returned by current ability. It’s defined in the resolve method. So if you run policy_scope(BookCopy), it returns all books if you’re an admin, or only borrowed books if you’re a user. Pretty cool, yeah?

We’re missing the borrow and the return_book methods. Let’s add them to the BookCopiesController:

Related Posts

As you've probably guessed by the title of my article, I still consider Ruby on Rails as a relevant technology that offers a lot of value, especially when combined with ReactJS as it's frontend counterpart. Here's how I approach the topic.

Gitlab Pipeline for Rails is the main part of a powerful GitLab CI/CD tool and can be a useful alternative for other applications like Jenkins and TeamCity. If you’re looking for some more detailed information on exactly how it works, we’ve compiled an example configuration that can help you.