These days, there are so many different choices when it comes to serving data from an API. You can build it in Node with ExpressJS, in Go with Martini, Clojure with Compojure, and many more. But in many cases, you just want to bring something to market as fast as you can. For those times, I still reach for Ruby on Rails.
With Rails, you can spin up a function API server in a very short period of time. Rails is large. Perhaps you object that there’s “too much magic”. Have you ever checked out the rails-api gem? It lets you enjoy all the benefits of Rails without including unnecessary view-layer and asset-related code.
Rails-api is maintained by Carlos Antonio Da Silva, Santiago Pastorino, Rails Core team members, and all-around great Rubyist Steve Klabnik. While not busy working on Rails or the Rails API Gem, they found the time to put together the active_model_serializers gem to make it easier to format JSON responses when using Rails as an API server.
ActiveModel::Serializers (AMS) is a powerful alternative to jbuilder, rabl, and other Ruby templating solutions. It’s easy to get started with, but when you want to serve data that quite doesn’t match up with the way ActiveRecord (AR) structures things, it can be hard to figure out how to get it to do what you want.
In this post, we’ll take a look at how to extend AMS to serve up custom data in the context of a Rails-based chat app.
Kicking It Off: Setting Up a Rails Server
Any two users in the system can have a continuous thread that goes back and forth. Let’s imagine we are building a chat app, similar to Apple’s Messages. People can sign up for the service, then chat with their friends.
To get started, we’ll run a
rails new, but since we’re using the
rails-api gem, we’ll need to make sure we have it installed first, with:
Once that’s done, we’ll run the following (familiar) command to start the project:
cd into the directory and setup the database with:
Creating the Models
We’ll need a couple of models:
Messages. The workflow to create them should be fairly familiar:
Open up the migrations, and set everything to
null: false, then run
We’ll also need to set up the relationships. Be sure to test these relationships (I would suggest using the shoulda gem to make it easy on yourself.
1 2 3 4
1 2 3 4
Serving the Messages
Let’s send some messages! Imagine for a minute that you’ve already set up some kind of token-based authentication system, and you have some way of getting ahold of the user that is making requests to your API.
We can open up the
MessagesController, and since we used a
scaffold, we should already be able to view all the messages. Let’s scope that to the current user. First we’ll write a convenience method to get all the sent and received messages for a user, then we’ll rework the
MessagesController to work the way we want it to.
1 2 3 4 5 6
1 2 3 4 5 6
Assuming that we have created a couple of sent and received messages for the
current_user, we should be able to take a look at
http://localhost:3000/messages and see some raw JSON that looks like this:
It’s kind of ugly. It would be nice if we could remove the timestamps and ids. This is where AMS comes in.
Once we add AMS to our project, it should be easy to get a much prettier JSON format back from our
To get AMS, add it to the
bundle install. Note that I’m using a the edge version of AMS here because it supports
belongs_to and other features. See the github project README for some information about maintenance and why you might want to use an older version.
Now we can easily set up a serializer with
rails g serializer message. Let’s take a look at what this generated for us. In
app/serializers/message_serializer.rb, we find this code:
1 2 3
attributes we specify (as a list of symbols) will be returned in the JSON response. Let’s skip
id, and instead return the
1 2 3
Now when we visit
/messages, we get this slightly cleaner JSON:
Cleaning Up the Format
It sure would be nice if we could get more information about the other user, like their username, so that we could display it in the messaging UI on the client-side. That’s easy enough, we just change the
MessageSerializer to use AR objects as attributes for the
recipient, instead of
1 2 3
Now we can see more about the Sender and Recipient:
Actually, that might be too much. Let’s clean up how
User objects are serialized by generating a User serializer with
rails g serializer user. We’ll set it up to just return the username.
1 2 3
MessageSerializer, we’ll use
recipient using the
1 2 3 4 5
If we take a look at
/messages, we now see:
Things are really starting to come together!
Although we can view all of a user’s messages using the
index controller action, or a specific message at the
show action, there’s something important to the business logic of our app that we can’t do. We can’t view all of the messages sent between two users. We need some concept of a
When thinking about creating a conversation, we have to ask, does this model need to be stored in the database? I think the answer is no. We already have messages that know which users they belong to. All we really need is a way to get back all the messages between two users from one endpoint.
We can use a Plain Old Ruby Object (PORO) to create this concept of a
conversation model. We will not inherit from
ActiveRecord::Base in this case.
Since we already know about the
current_user, we really only need it to keep track of the other user. We’ll call her the
1 2 3 4 5 6 7 8 9
We’ll want to be able to serve up these conversations, so we’ll need a
ConversationsController. We want to get all of the conversations for a given user, so we’ll add a class-level method to the
Conversation model to find them and return them in this format:
To make this work, we’ll run a
group_by on the user’s messages, grouping by the other user’s id. We’ll then map the resulting hash into a collection of
Conversation objects, passing in the other user and the list of messages.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
If we run this in the Rails Console, it seems to be working.
1 2 3
Great! We’ll just call this method in our
ConversationsController and everything will be great!
First, we’ll define the route in
1 2 3 4
Then, we’ll write the controller action.
1 2 3 4 5 6 7 8
/conversations, we should see a list of all the conversations for the current user.
Serializing Plain Old Ruby Objects
Whoops! When we visit that route, we get an error:
undefined methodnew’ for nil:NilClass`. It’s coming from this line in the controller:
It looks like the error is coming from the fact that we don’t have a serializer. Let’s make one with
rails g serializer conversation. We’ll edit it to return its attributes,
1 2 3
Now when we try, we get another error, coming from the same line of the controller:
undefined method 'read_attribute_for_serialization' for #<Conversation:0x007ffc9c1bed10>
Digging around in the source code for ActiveModel::Serializers, I couldn’t find where that method was defined. So I took a look at ActiveModel itself, and found it here. It turns out that it’s just an alias for
We can add that into our PORO easily enough:
1 2 3 4
Or, we could
include ActiveModel::Serialization which is where our AR-backed objects got it.
Now when we take a look at
/conversations, we get:
Whoops. Not quite right. But the problem is similar to the one we had before in the
MessageSerializer. Maybe the same approach will work. We’ll change the
attributes to AR relationships.
1 2 3 4
We can’t see who the sender of each message was! AMS isn’t using the
UserSerializer for the message sender and recipient, because we’re not using an AR object.
A little source code spelunking point the way to a fix.
1 2 3 4 5 6 7 8 9 10 11
/conversations gives us what we want:
/messages still works as well!
The ActiveModel::Serializers gem claims to bring “convention over configuration to your JSON generation”. It does a great job of it, but when you need to massage the data, things can get a little bit hairy.
Hopefully some of the tricks we’ve covered will help you present JSON from your Rails API the way you want. For this, and virtually any other problem caused by the magic getting in the way, I can’t suggest digging through the source code enough.
At the end of the day ARS is an excellent choice for getting your JSON API off the ground with a minimum of fuss. Good luck!