When building an an API, the consumer (the developer) doesn't have any visual queues in order how to user your application, so, if they are not to solely rely on documentation, your interface needs to be intuitive. One way to get far with this is to be consistent in the objects that you respond with. One way to achieve this in rails is to use Active Model Serializers.
To install simply add to your Gemfile
gem 'active_model_serializers'
And run 'bundle install', now when you render json, rails will look for a serializer for the object it's trying to convert to json. Let's say you have a 'Car' model. To generate a serializer for this it's simply
rails g serializer Car
this will generate a file in serializers within your app folder, for example.
class CarSerializer < ActiveModel::Serializer
attributes :id, :color, :engine_id, :number_of_doors
end
An example of the output of this is:
{
"car": {
"color": "black",
"engine_id": 1,
"number_of_doors: 5
}
}
This is saying that we should include the attributes listed, if the Car model also had, lets say a weight property, this will not be output as it is not within the attributes listed.
Adding a property
Lets say we want to add a computed property to the serializer, it could be a property related to the context of the current request, so might not fit within the model itself, you'll need to make a judgement on that, but if we wanted to add an 'is_dark' attribute, we could do so as below:
class CarSerializer < ActiveModel::Serializer
attributes :id, :color, :engine_id, :number_of_doors, :is_dark
def is_dark
object.color == "black" || object.color == "brown"
end
end
Notice we have added the isdark property to the list of attributes, but we have also defined a method called isdark to return our desired value. Note this will also override an existing model function, however, I strongly would discourage that. The above serializer would output
{
"car": {
"color": "black",
"engine_id": 1,
"number_of_doors: 5,
"is_dark": true
}
}
Embedding Children
Now, as we are building an API we want it to be efficient for the consuming application, so sometimes we need to embed child objects within the one response to avoid N+1 performance issues.
Our Car class has a related object of Engine, which we would like to embed, this is as simple as calling its serializer to generate the engine json object.
class CarSerializer < ActiveModel::Serializer
attributes :id, :color, :engine, :number_of_doors, :is_dark
def is_dark
object.color == "black" || object.color == "brown"
end
def engine
EngineSerializer.new(object.engine)
end
end
We have now changed engine_id in the attributes call to engine and added a method below for this. The method creates a new EngineSerializer which you would also have created and setup for an Engine class. We need to pass in the object to serialize, in this case it is the related engine for our car, this outputs:
{
"car": {
"color": "black",
"engine": {
"size": 2300,
"fuel_type": "diesel"
},
"number_of_doors: 5,
"is_dark": true
}
}
I've found this to be an excellent way of packaging up your objects to make API's predictable and easier to consume. Testing is also easier, and I also use json-schema which i use to test the output of all controllers to make sure that they all conform to the documentation.