How to Wire Up Ruby on Rails and AngularJS as a Single-Page Application (Gulp Version)

Why this tutorial exists

I wrote this tutorial because I had a pretty tough time getting Rails and Angular to talk to each other as an SPA. The best resource I could find out there was Ari Lerner’s Riding Rails with AngularJS. I did find that book very helpful and I thought it was really well-done, but it seems to be a little bit out-of-date by now and I couldn’t just plug in its code and have everything work. I had to do a lot of extra Googling and head-scratching to get all the way there. This tutorial is meant to be a supplement to Ari’s book, not a replacement for it. I definitely recommend buying the book because it really is very helpful.

Refreshed for 2015

I had written a tutorial with almost the same title last summer. In that tutorial I used Grunt instead of Gulp, HTML instead of HAML or Jade, and regular JavaScript instead of CoffeeScript or ES6. Not only do cooler alternatives to those traditional technologies exist today, they did back then, too. My tutorial was sorely in need of a reboot. So here it is.

The sample app

There’s a certain sample app I plan to use throughout AngularOnRails.com called Lunch Hub. The idea with Lunch Hub is that office workers can announce in the AM where they’d like to go for lunch rather than deciding as they gather around the door and waste half their lunch break. Since Lunch Hub is a real project with its own actual production code, I use a different project here called “Fake Lunch Hub.” You can see the Fake Lunch Hub repo here.

Setting up our Rails project

Instead of regular Rails we’re going to use Rails::API. I’ve tried to do Angular projects with full-blown Rails, but I end up with a bunch of unused views, which feels weird. First, if you haven’t already, install Rails::API.

1$ gem install rails-api

Creating a new Rails::API project works the same as creating a regular Rails project.

1$ rails-api new fake_lunch_hub -T -d postgresql

Get into our project directory.

1$ cd fake_lunch_hub

Create our PostgreSQL user.

1$ createuser -P -s -e fake_lunch_hub

Create the database.

1$ rake db:create

Now we’ll create a resource so we have something to look at through our AngularJS app. (This might be a good time to commit this project to version control.)

Creating our first resource

Add gem 'rspec-rails' to your Gemfile (in the test group) and run:

12$ bundle install$ rails g rspec:install

When you generate scaffolds from now on, RSpec will want to create all kinds of spec files for you automatically, including some kinds of specs (like view specs) that in my opinion are kind of nutty and really shouldn’t be there. We can tell RSpec not to create these spec files:

123456789101112131415161
718192021222324252
6272829303132
3334353637383940
require File.expand_path(‘../boot’, __FILE__) # Pick the frameworks you want:require “active_model/railtie”require “active_record/railtie”require “action_controller/railtie”require “action_mailer/railtie”require “action_view/railtie”require “sprockets/railtie”# require “rails/test_unit/railtie” # Require the gems listed in Gemfile, including any gems# you’ve limited to :test, :development, or :production.Bundler.require(*Rails.groups) module FakeLunchHub  class Application < Rails::Application    # Settings in config/environments/* take precedence over those specified here.    # Application configuration should go into files in config/initializers    # — all .rb files in that directory are automatically loaded.     # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.    # Run “rake -D time” for a list of tasks for finding time zone names. Default is UTC.    # config.time_zone = ‘Central Time (US & Canada)’     # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.    # config.i18n.load_path += Dir[Rails.root.join(‘my’, ‘locales’, ‘*.{rb,yml}’).to_s]    # config.i18n.default_locale = :de     config.generators do |g|      g.test_framework :rspec,        fixtures: false,        view_specs: false,        helper_specs: false,        routing_specs: false,        request_specs: false,        controller_specs: true    end  endend

(Now might be another good time to make a commit.)

In Lunch Hub, I want everybody’s lunch announcements to be visible only to other people in the office where they work, not the whole world. And there’s actually a good chance a person might want to belong not only to a group tied to his or her current workplace, but perhaps a former workplace or totally arbitrary group of friends. So I decided to create the concept of a Group in Lunch Hub. Let’s create a Group resource that, for simplicity, has only one attribute: name.

1$ rails g scaffold group name:string

Since groups have to have names, let’s set null: false in the migration. We’ll also include a uniqueness index.

1234567891011class CreateGroups < ActiveRecord::Migration  def change    create_table :groups do |t|      t.string :name, null: false       t.timestamps    end     add_index :groups, :name, unique: true  endend
1$ rake db:migrate

Now, if you run rails server and go to http://localhost:3000/groups, you should see empty brackets ([]). We actually want to be able to do http://localhost:3000/api/groups instead.

12345Rails.application.routes.draw do  scope ‘/api’ do    resources :groups, except: [:new, :edit]  endend

At the risk of being annoying, I wanted to include a realistic level of testing in the tutorial, at least on the server side.

1234567891011121314require ‘rails_helper’ RSpec.describe Group, :type => :model do  before do    @group = Group.new(name: “Ben Franklin Labs”)  end   subject { @group }   describe “when name is not present” do    before { @group.name = ” ” }    it { should_not be_valid }  endend

To make this spec pass you’ll of course need to add a validation:

123class Group < ActiveRecord::Base  validates :name, presence: trueend

We also have to adjust the controller spec RSpec spit out for us because RSpec’s generators are evidently not yet fully compatible with Rails::API. The generated spec contains an example for the new action, even though we don’t have a new action. You can remove that example yourself or you can just copy and paste my whole file.

12345678910111213141516171819202122232
42526272829303132333435363738394041424
34445464748495051525354555657585960616
26364656667686970717273747576777879808
18283848586878889909192939495969798991
00101102103104105106107108109110111112
11311411511611711811912012112212312412
51261271281291301311321331341351361371
381391401411421431441451461471481491501
51152
require ‘rails_helper’ # This spec was generated by rspec-rails when you ran the scaffold generator.# It demonstrates how one might use RSpec to specify the controller code that# was generated by Rails when you ran the scaffold generator.## It assumes that the implementation code is generated by the rails scaffold# generator.  If you are using any extension libraries to generate different# controller code, this generated spec may or may not pass.## It only uses APIs available in rails and/or rspec-rails.  There are a number# of tools you can use to make these specs even more expressive, but we’re# sticking to rails and rspec-rails APIs to keep things simple and stable.## Compared to earlier versions of this generator, there is very limited use of# stubs and message expectations in this spec.  Stubs are only used when there# is no simpler way to get a handle on the object needed for the example.# Message expectations are only used when there is no simpler way to specify# that an instance is receiving a specific message. RSpec.describe GroupsController, :type => :controller do   # This should return the minimal set of attributes required to create a valid  # Group. As you add validations to Group, be sure to  # adjust the attributes here as well.  let(:valid_attributes) {    skip(“Add a hash of attributes valid for your model”)  }   let(:invalid_attributes) {    skip(“Add a hash of attributes invalid for your model”)  }   # This should return the minimal set of values that should be in the session  # in order to pass any filters (e.g. authentication) defined in  # GroupsController. Be sure to keep this updated too.  let(:valid_session) { {} }   describe “GET index” do    it “assigns all groups as @groups” do      group = Group.create! valid_attributes      get :index, {}, valid_session      expect(assigns(:groups)).to eq([group])    end  end   describe “GET show” do    it “assigns the requested group as @group” do      group = Group.create! valid_attributes      get :show, {:id => group.to_param}, valid_session      expect(assigns(:group)).to eq(group)    end  end   describe “GET edit” do    it “assigns the requested group as @group” do      group = Group.create! valid_attributes      get :edit, {:id => group.to_param}, valid_session      expect(assigns(:group)).to eq(group)    end  end   describe “POST create” do    describe “with valid params” do      it “creates a new Group” do        expect {          post :create, {:group => valid_attributes}, valid_session        }.to change(Group, :count).by(1)      end       it “assigns a newly created group as @group” do        post :create, {:group => valid_attributes}, valid_session        expect(assigns(:group)).to be_a(Group)        expect(assigns(:group)).to be_persisted      end       it “redirects to the created group” do        post :create, {:group => valid_attributes}, valid_session        expect(response).to redirect_to(Group.last)      end    end     describe “with invalid params” do      it “assigns a newly created but unsaved group as @group” do        post :create, {:group => invalid_attributes}, valid_session        expect(assigns(:group)).to be_a_new(Group)      end       it “re-renders the ‘new’ template” do        post :create, {:group => invalid_attributes}, valid_session        expect(response).to render_template(“new”)      end    end  end   describe “PUT update” do    describe “with valid params” do      let(:new_attributes) {        skip(“Add a hash of attributes valid for your model”)      }       it “updates the requested group” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => new_attributes}, valid_session        group.reload        skip(“Add assertions for updated state”)      end       it “assigns the requested group as @group” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => valid_attributes}, valid_session        expect(assigns(:group)).to eq(group)      end       it “redirects to the group” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => valid_attributes}, valid_session        expect(response).to redirect_to(group)      end    end     describe “with invalid params” do      it “assigns the group as @group” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => invalid_attributes}, valid_session        expect(assigns(:group)).to eq(group)      end       it “re-renders the ‘edit’ template” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => invalid_attributes}, valid_session        expect(response).to render_template(“edit”)      end    end  end   describe “DELETE destroy” do    it “destroys the requested group” do      group = Group.create! valid_attributes      expect {        delete :destroy, {:id => group.to_param}, valid_session      }.to change(Group, :count).by(-1)    end     it “redirects to the groups list” do      group = Group.create! valid_attributes      delete :destroy, {:id => group.to_param}, valid_session      expect(response).to redirect_to(groups_url)    end  end end

Now if you run all specs on the command line ($ rspec), they should all pass. We don’t have anything interesting to look at yet but our Rails API is now good to go.

Adding the client side

On the client side we’ll be using Yeoman, a front-end scaffolding tool. First, install Yeoman itself as well as generator-gulp-angular. (If you don’t already have npm installed, you’ll need to do that. If you’re using Mac OS with Homebrew, run brew install npm.)

12$ npm install -g yo$ npm install -g generator-gulp-angular

We’ll keep our client-side code in a directory called client. (This is an arbitrary naming choice and you could call it anything.)

1$ mkdir client && cd $_

Now we’ll generate the Angular app itself. When I ran it, I made the following selections:

  • Angular version: 1.3.x
  • Modules: all
  • jQuery: 2.x
  • REST resource library: ngResource (just because it’s the default and angularjs-rails-resource isn’t an option on the list)
  • Router: UI Router
  • UI framework: Bootstrap
  • Bootstrap component implementation: Angular UI
  • CSS preprocessor: Sass (Node)
  • JS preprocessor: CoffeeScript
  • HTML template engine: Jade
1$ yo gulp-angular fake_lunch_hub

If you have a Rails server running on port 3000, stop it for now because Gulp will also run on port 3000 by default. Start Gulp to see if it works:

1$ gulp serve

Gulp should now open a new browser tab for you at http://localhost:3000/#/ where you see the “‘Allo, ‘Allo” thing. Our Angular app is now in place. It still doesn’t know how to talk to Rails, so we still have to make that part work.

Setting up a proxy

The only way our front-end app (Angular and friends) will know about our back-end server (Rails) is if we tell our front-end app about our back-end app. The basic idea is that we want to tell our front-end app to send any requests to http://our-front-end-app/api/whatever to http://our-rails-server/api/whatever. Let’s do that now.

If you look inside client/gulp, you’ll notice there’s a file in there called proxy.js. I would like to have simply tweaked this file slightly to get our proxy working, but unfortunately I found proxy.js very confusing and difficult to work with. So I deleted it and set up the proxy a different way. Let’s delete proxy.js so it doesn’t confuse future maintainers.

1$ rm gulp/proxy.js

You’ll notice another file inside client/gulp called server.js. I found that minimal adjustment in this file was necessary in order to get the proxy working. Here’s what my server.js looks like after my modifications, which I’ll explain:

123456789101112131415161718192021222324252627282930313
233343536373839404142434445464748495051525354555657585960616263
‘use strict’; var gulp = require(‘gulp’);var browserSync = require(‘browser-sync’);var browserSyncSpa = require(‘browser-sync-spa’);var util = require(‘util’);var proxyMiddleware = require(‘http-proxy-middleware’);var exec = require(‘child_process’).exec; module.exports = function(options) {   function browserSyncInit(baseDir, browser) {    browser = browser === undefined ? ‘default’ : browser;     var routes = null;    if(baseDir === options.src || (util.isArray(baseDir) && baseDir.indexOf(options.src) !== -1)) {      routes = {        ‘/bower_components’: ‘bower_components’      };    }     var server = {      baseDir: baseDir,      routes: routes,      middleware: [        proxyMiddleware(‘/api’, { target: ‘http://localhost:3000’ })      ]    };     browserSync.instance = browserSync.init({      port: 9000,      startPath: ‘/’,      server: server,      browser: browser    });  }   browserSync.use(browserSyncSpa({    selector: ‘[ng-app]’// Only needed for angular apps  }));   gulp.task(‘rails’, function() {    exec(“rails server”);  });   gulp.task(‘serve’, [‘watch’], function () {    browserSyncInit([options.tmp + ‘/serve’, options.src]);  });   gulp.task(‘serve:full-stack’, [‘rails’, ‘serve’]);   gulp.task(‘serve:dist’, [‘build’], function () {    browserSyncInit(options.dist);  });   gulp.task(‘serve:e2e’, [‘inject’], function () {    browserSyncInit([options.tmp + ‘/serve’, options.src], []);  });   gulp.task(‘serve:e2e-dist’, [‘build’], function () {    browserSyncInit(options.dist, []);  });};

Here are the things I changed, it no particular order:

  1. Configured BrowserSync to run on port 9000 so Rails can run on its default port of 3000 without conflicts
  2. Added middleware that says “send requests to /api to http://localhost:3000
  3. Added a rails task that simply invokes the rails server command
  4. Added a serve:full-stack task that runs the regular old serve task, but first runs the rails task

You’ll have to install http-proxy-middleware before continuing:

1$ npm install –save-dev http-proxy-middleware

Now we can run our cool new task. Make sure neither Rails nor Gulp is already running somewhere.

1$ gulp serve:full-stack

Three things should now happen:

  1. The front-end server should come up on port 9000 instead of 3000.
  2. If you navigate to http://localhost:9000/api/foo, you should get a Rails page that says No route matches [GET] "/api/foo", which means
  3. Rails is running on port 3000.

Getting Rails data to show up in our client app

Now we’ll want to get some actual data to show up in the actual HTML of our Angular app. This is a pretty easy step now that we have the plumbing taken care of. First, create some seed data:

1234567891011121314# This file should contain all the record creation needed to seed the database with its default values.# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).## Examples:##   cities = City.create([{ name: ‘Chicago’ }, { name: ‘Copenhagen’ }])#   Mayor.create(name: ‘Emanuel’, city: cities.first) Group.create([  { name: ‘Ben Franklin Labs’ },  { name: ‘Snip Salon Software’ },  { name: ‘GloboChem’ },  { name: ‘TechCorp’ },])

Get the data into the database:

1$ rake db:seed

Now let’s modify src/app/index.coffee to include a state for groups:

12345678910111213angular.module ‘fakeLunchHub’, [‘ngAnimate’, ‘ngCookies’, ‘ngTouch’, ‘ngSanitize’, ‘ngResource’, ‘ui.router’, ‘ui.bootstrap’]  .config ($stateProvider, $urlRouterProvider) ->    $stateProvider      .state “home”,        url: “/”,        templateUrl: “app/main/main.html”,        controller: “MainCtrl”      .state “groups”,        url: “/groups”,        templateUrl: “app/views/groups.html”,        controller: “GroupsCtrl”     $urlRouterProvider.otherwise ‘/’

Then we add GroupsCtrl, which at this point is almost nothing:

12angular.module “fakeLunchHub”  .controller “GroupsCtrl”, ($scope) ->

(I manually created a new directory for this, src/app/controllers.)

Lastly, let’s create a view at src/app/views/groups.jade:

123div.container  div(ng-include=”‘components/navbar/navbar.html'”)  h1 Groups

If you now navigate to http://localhost:9000/#/groups, you should see a big h1 that says “Groups”. So far we’re not talking to Rails at all yet. That’s the very next step.

A good library for Angular/Rails resources is called, straightforwardly, angularjs-rails-resource. It can be installed thusly:

1$ bower install –save angularjs-rails-resource

Now let’s add two things to src/app/index.coffee: the rails module and a resource called Group.

1234567891011121314151617angular.module ‘fakeLunchHub’, [‘ngAnimate’, ‘ngCookies’, ‘ngTouch’, ‘ngSanitize’, ‘ngResource’, ‘ui.router’, ‘ui.bootstrap’, ‘rails’]  .config ($stateProvider, $urlRouterProvider) ->    $stateProvider      .state “home”,        url: “/”,        templateUrl: “app/main/main.html”,        controller: “MainCtrl”      .state “groups”,        url: “/groups”,        templateUrl: “app/views/groups.html”,        controller: “GroupsCtrl”     $urlRouterProvider.otherwise ‘/’   .factory “Group”, (RailsResource) ->    class Group extends RailsResource      @configure url: “/api/groups”, name: “group”

Now let’s add a line to our controller to make the HTTP request:

123angular.module “fakeLunchHub”  .controller “GroupsCtrl”, ($scope, Group) ->    Group.query().then (groups) -> $scope.groups = groups

And some code in our template to show the group names:

123456div.container  div(ng-include=”‘components/navbar/navbar.html'”)  h1 Groups   ul(ng-repeat=”group in groups”)    li {{ group.name }}

If you now visit http://localhost:9000/#/groups, you should see your group names there. Congratulations! You just wrote a single-page application. It’s a trivial and useless single-page application, but you’re off to a good start. In my experience the plumbing is the hardest part.

That’s all for now

I’ve heard requests for tutorials on basic CRUD operations in Angular/Rails, so keep your eye out for that in the near future. You can subscribe to my posts by leaving your email in the upper right corner.

Also, if you enjoyed this tutorial, you may also like my webinar version of this same tutorial (and then some). Thanks for reading.