Building a Rails API for Use with Ember
While I was a student at the Flatiron School, I was introduced to Ember, a front-end framework to simplify and streamline all the clunky, client-side JavaScript you end up using to build highly responsive and fast user experiences. Unfortunately, because we only spent a week on it, it felt like we only dipped our toes in the water, so I decided to tackle an Ember project myself to apply some of the concepts I learned as well as learn some new tricks. Here are the results of that exploration.
If you know me at all, you'll also know that I'm obsessed with Downton Abbey. So for this project, I wanted to build an app to organize some of my favorite quotes from the show, as well as provide synopses of each episode and overviews of each (important) character. You can see my in-progress app hosted right here on Heroku. It may take a second to boot up because, as I've also learned, making browsers download all that JavaScript up front takes time.
But first...the back end.
##How to Build a Rails API For the back end, I built a Rails API. Here's the code. On the Rails side, I scraped the Downton Abbey Wikia API and also scraped one of the community site's pages to create interrelated instances of Characters, Episodes, and Seasons. That was an adventure by itself, but it provided me with some good seed data that I could later edit and build upon.
As far as building the API went, I used the rails-api
gem, which has been deprecated but still works. This helps you set up your controllers to inherit from ActionController::API
rather than ActionController::Base
, which means it has fewer built-in methods and is a little lighter. At first, I had namespaced my API under api/v1, but after encountering several issues with trying to incorporate authentication into my app, I decided to un-namespace everything. Through Googling, I found that there are ways to get around this, because Devise was throwing me for a loop, but I haven't gotten around to fixing it yet. All in due time!
After adding routes for all of my resources, I also defined the standard CRUD actions in the controllers for each resource respectively. And then I moved on to the serializers, which let you decide how to format the JSON data that the Rails API makes available. With an API, you of course render data back instead of a view.
###Serializers
All of my serializers inherit from ActiveModel:Serializer.
And within each serializer, I decided which attributes on each model to make public.
Only on the Character model did I do any sideloading -- that's when you tell your API to load certain related models alongside the primary data for that specific route.
For instance, this is what the Character Serializer looks like:
class CharacterSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :name, :title, :social_class, :actor_id, :image_url, :bio
has_many :quotes
has_one :actor
has_one :family
end
The has_many
and has_one
relationships listed here load up the quotes, actor, and family.
Interestingly, the line embed :ids, include: true
determines whether, within the models, the related models are also loaded, or if their ids only are loaded.
For example, when you include embed :ids, include: true
, the property 'quote_ids':[] appears within the JSON. When you leave it out, you'll get the quotes themselves as JSON.
Watch out for sideloading data on multiple serializers. It can be easy to create an infinite loop and crash your application. Since I called up the "has_many" relationship here, if I did the same thing from the Actor serializer, I'd end up with a serious problem. In this application, I didn't make use of the Actor model too much, so I'll provide the example of Seasons and Episodes.
In the Episode Serializer, to avoid an infinite loop, I ended up writing an attributes method within the Episode Serializer. This is what the code looks like:
class EpisodeSerializer < ActiveModel::Serializer
attributes :id, :name, :summary, :season_id, :image_url
has_many :quotes
def attributes
attributes = super
attributes[:season] = object.season.try(:as_json)
attributes
end
end
By using super
, we can override the superclass attributes method and define how we want it to behave ourselves. In other words, we'll turn the related season into JSON, and not cause season to also load the episode. Cool. That solves that problem. (By the way, props to my instructor Jeff for teaching us this magic.)
###CORS
Oh, by the way, when you're building an API with Rails, you'll need to make the app respond to requests from trusted sources. Various sources suggest that you use the rack-cors
gem to help. Throw that in your Gemfile, and then within the file application.rb
you'll need to include this code to enable cross-origin resource sharing.
config.middleware.insert_before 0, "Rack::Cors" do
allow do
origins '*'
resource '*', :headers => :any, :methods => [:get, :post, :options, :put, :patch, :delete]
end
end
Yes, this enables requests from all sources, not just trusted ones. Yes, that's bad. I had some issues configuring it with Heroku though, so it'll have to do for now.
Okay, enough of the API side. Now it's time to move over to Ember land in the next post.