How to integrate AngularJS with Rails 4

Building most single-page applications (SPAs for short) is a two-step process: first you create a JSON API in a backend technology of choice and then you use that API in the JavaScript application. Here we'll be using Ruby on Rails on the backend and AngularJS on the frontend.

The main pain point of any kind of integration is making sure that everything fits together well. This post will not take you through building the whole application. Instead, it will focus on making sure all the integration points are handled properly. I will also share with you some practical advice on the topic.

Code examples used in this post come from a Todo list management application. This text summarizes all the lessons learned during writing of that app.

Building a JSON API in Rails

Building an API in Rails is easy, so we'll roll our own from scratch. Note that if you decide to use a specialized library like angularjs-rails-resource some details will differ, but the general idea will remain the same.

Routing

Let's start by defining routes for our API.

namespace :api, defaults: {format: :json} do
  resources :task_lists, only: [:index] do
    resources :tasks, only: [:index, :create, :update, :destroy]
  end
end

This is all pretty standard. We can get all lists through the task_lists#index action, get a task listing for a specific list via tasks#index action and operate on specific tasks via create, update and destroy actions. Using format: :json is a handy default.

If we run rake routes now, we will get an output similar to this:

GET    /api/task_lists/:task_list_id/tasks(.:format)     api/tasks#index {:format=>:json}
POST   /api/task_lists/:task_list_id/tasks(.:format)     api/tasks#create {:format=>:json}
PATCH  /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#update {:format=>:json}
PUT    /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#update {:format=>:json}
DELETE /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#destroy {:format=>:json}
GET    /api/task_lists(.:format)                         api/task_lists#index {:format=>:json}

There are two HTTP verbs corresponding to the update action: PATCH and PUT. Supporting PATCH is a new feature added in Rails 4.0. You can read more about it on the offical blog.

Request parameters

Rails 4 also changed the way the mass-assignment protection is done. Instead of whitelisting/blacklisting parameters in the model, you now have to do it in the controller, using require and permit methods. I like to create a helper method than can be used both in create and update actions:

def safe_params
  params.require(:task).permit(:description, :priority, :completed)
end

With this definition, action implementation looks as simple as this:

def create
  task = task_list.tasks.create!(safe_params)
  render json: task, status: 201
end

def update
  task.update_attributes(safe_params)
  render nothing: true, status: 204
end

Generating JSON

The previous example already hinted at this: returning JSON output should be as simple as writing render json: object. I like to use active_model_serializers gem which greatly simplifies the process. Whenever you render an object or a collection of objects to json, a proper serializer will be used. In case of our Todo list application, the following will render an array of tasks:

render json: TaskList.find(params[:id]).tasks

To get the exact format we want (that will be easy to consume by AngularJS), after installing the gem, we also need to configure it. Put the following into config/initializers/active_model_serializers.rb:

ActiveSupport.on_load(:active_model_serializers) do
  # Disable for all serializers (except ArraySerializer)
  ActiveModel::Serializer.root = false

  # Disable for ArraySerializer
  ActiveModel::ArraySerializer.root = false
end

With this configuration in place and a serializer defined like that:

# app/serializers/task_serializer.rb
class TaskSerializer < ActiveModel::Serializer
  attributes :id, :description, :priority, :due_date, :completed
end

we'll get the following output:

[
 {'id' => 123,
  'description' => 'Send newsletter',
  'priority' => 2,
  'due_date' => '2013-09-10',
  'completed' => true},
 {'id' => 124,
  'description' => 'Prepare presentation',
  'priority' => 1,
  'due_date' => '2013-09-17',
  'completed' => false}
]

Testing

All respectable APIs have to be well tested. Fortunately, Rails makes writing automated tests really easy. In case of a JSON API, controller tests are the way to go. That's how a sample test may look like, using RSpec syntax:

describe Api::TasksController do
  it "should be able to create a new record" do
    post :create, task_list_id: task_list.id,
      task: {description: "New task"}, format: :json
    response.should be_success
    JSON.parse(response.body).should == {'id' => 123, ...}
  end
end

An important detail to note here is the use of format: :json. This makes sure that the parameters are passed and interpreted as JSON.

When writing more tests like this, you may find it useful to define a helper method for parsing the response. Put the following into your spec_helper.rb:

module JsonApiHelpers
  def json_response
    @json_response ||= JSON.parse(response.body)
  end
end

RSpec.configure do |config|
  config.include JsonApiHelpers, type: :controller
end

With this code in place, instead of:

JSON.parse(response.body).should == {...}

you can now write:

json_response.should == {...}

which is a little cleaner, you must admit. It also has an added bonus that the response will be only parsed once, even if you make multiple assertions on the output.

Building AngularJS application

Since the API is ready it's finally time to move on to building the AngularJS application. There is a breadth of tutorials to watch and read, so I'm not going to repeat that here, instead focusing solely on the integration with Rails.

Including AngularJS files

The fastest way to get started is putting the JavaScript include tags for AngularJS directly into layout. At the time of writing this post, 1.0.8 is the latest stable version, so if you want to use that, put the following two lines into app/views/layouts/application.html.slim:

= javascript_include_tag "//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"
= javascript_include_tag "//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular-resource.min.js"

Of course you can also download the files and put them somewhere in app/assets/javascripts/. Unfortunately the asset pipeline may break some of your AngularJS code due to renaming. To prevent that, put the following line into your config/environments/production.rb:

config.assets.js_compressor = Uglifier.new(mangle: false)

This will disable name mangling during JavaScript minification. You can read more about this topic in the official tutorial (scroll down to "A Note on Minification").

Structuring the AngularJS code

Each AngularJS application consists of the main application module and some controllers, directives and services. As long as you keep everything under app/assets/javascripts/ the asset pipeline will put them all together without a problem. Ultimately it's up to you where to put each of them, but here's how I've done it.

First, my application.js lists all the external requirements (like jQuery or AngularJS itself), then the file containing the main application module, to finally use the require_tree directive:

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require lib/angular.min
//= require lib/angular-resource.min
//= require todoApp
//= require_tree .

With that in mind, the main application module is defined in todoApp.js.coffee and looks like this:

todoApp = angular.module('todoApp', ['ngResource'])

I keep the rest of the files in suitable subdirectories: controllers, directives and services for standard elements of an AngularJS app, and lib for any other dependencies.

Defining the service

The Rails API can be accessed from the AngularJS app through the ngResource module. Instead of using the resource directly in the controller, it's a good practice to define a service around it. This way you can abstract away some pesky details of accessing data, much like you would do with Rails models.

Below is a basic service for accessing tasks, written in CoffeeScript.

angular.module('todoApp').factory 'Task', ($resource) ->
  class Task
    constructor: (taskListId) ->
      @service = $resource('/api/task_lists/:task_list_id/tasks/:id',
        {task_list_id: taskListId, id: '@id'})

    create: (attrs) ->
      new @service(task: attrs).$save (task) ->
        attrs.id = task.id
      attrs

    all: ->
      @service.query()

For example, to get a list of all tasks from a given list, you'd do the following:

$scope.tasks = Task(taskListId).all()

It cannot get any easier than this.

Making it work with CSRF protection

Rails come with cross-site request forgery protection in the form of a token embeded in the head section of each page. To make forms work in AngularJS you need to use that token in all API requests. Put the following three lines into the main application file (todoApp.js.coffee in our case):

todoApp.config ($httpProvider) ->
  authToken = $("meta[name=\"csrf-token\"]").attr("content")
  $httpProvider.defaults.headers.common["X-CSRF-TOKEN"] = authToken

Making it work with turbolinks

Turbolinks which became a default in Rails 4 may cause some problems to AngularJS applications, especially if you need to support different SPAs on multiple pages. To overcome this problem, put the following into the main application file:

$(document).on 'page:load', ->
  $('[ng-app]').each ->
    module = $(this).attr('ng-app')
    angular.bootstrap(this, [module])

This will make sure the AngularJS application is properly initialized each time a turbolink does its fetch&replace magic.

Making updates using the PATCH method

The new PATCH method mentioned in the beginning of this post is not supported by ngResource by default, but it's easy enough to make it work. First, put the following code into the main application file:

defaults = $http.defaults.headers
defaults.patch = defaults.patch || {}
defaults.patch['Content-Type'] = 'application/json'

It will ensure any PATCH requests are made with application/json content type.

After that, modify the resource definition from before, so that it specifies PATCH as a prefered verb for the update action.

$resource('/api/task_lists/:task_list_id/tasks/:id',
  {task_list_id: taskListId, id: '@id'},
  {update: {method: 'PATCH'}})

Now, whenever you issue an update on the resource, it will properly submit a PATCH request with JSON content.

Testing

Just as Rails, AngularJS has a great testing story. Thanks to its focus on Dependency Injection, unit testing components of an AngularJS application is a breeze.

The official tutorial walks you through setting up testing infrastructure, using Karma, so I'm not going to repeat that here. I found it easy to use with Jasmine which I already knew and with angular-mocks which helps with mocking some features of a web browser.

Debugging

When testing fails it's often useful to be able to boot up the browser and poke around manually. As I was learning AngularJS and figuring out integration problems, Misko Hevery's answer on Stackoverflow was a big help to me.

Turns out, inspecting AngularJS app internals from the browser is not that complicated. All you need to do is to grab an element with jQuery. For example, that's how you can access scope in the context of the taskDescription element:

$("#taskDescription").scope()

From there you can traverse the complete state of your controller.

Another tool that may come in handy is AngularJS Batarang, a Chrome extension that allows you to inspect and profile your SPA's internals.

Now go and build!

That should get you through the initial steps of building your dream single-page application.

Leave your thoughts in the comments and if you need a hosting for your Rails backend you don't have to look far. :)


Shelly Cloud is a platform for hosting Ruby and Ruby on Rails applications. You can focus on development without getting distracted by deployment, optimization and maintenance.


Want to be up-to-date with Shelly?

Follow us on Twitter and get latest news about platform, ongoing issues and blog posts.

Talk to a human