"best practices" for Rails serving RESTful JSON services for use by AngularJS, Ember.js, etc.

I’ve been writing a gem to implement and extend common controller functionality so that Rails can be used with Javascript frameworks like AngularJS (which we are using), Ember.js, etc. in such a way that the user doesn’t have to tweak a a bunch of rails g controller boilerplate code to provide services for use in these frameworks that in turn would require various changes to fit the normal Rails way to specify things.

The gem is here:

It needs a heck of a lot of work still, no working tests at the moment, and integrating roar-rails in its 3.0.0 branch.

Our aim/current strategy for web development is to:

  1. Keep # of controllers (and amount of code required for each) to a minimum, but they shouldn’t be overly monolithic to the point that they become difficult to maintain.
  2. (Only) use REST where it makes sense.
  3. Try to keep logic and operational knowledge out of the client side.

More specifically:

  • Unlike the typical historical Rails app where the controller was really the controller accessing the model and serving up the view, when you are using AngularJS and Ember.js heavily, the primary role of Rails REST-ish service provider; not everything fits REST (hypertext/hypermedia doesn’t fit every application) and there are definitely going to be custom actions.
  • Services need to be able to be defined quickly and need to return errors in JSON format with relevant status codes, etc.
  • You need to be able to transactionally make alterations to associations and collections of associations, not just to a single resource.
  • The services also should make it as easy as possible to integrate with these frameworks. To persist an associated model you shouldn’t have to tack on _attributes to the key in the JSON because odds are that the Javascript app is just storing it as whatever the association name is, or perhaps some other name that makes more sense. You should be able to send in the JSON assocation data for something and the controller should know via mass assignment security that you don’t have rights to write the association, but you do have rights to change the list of associations, etc. so it would just change those associations if you told it to allow that, etc. In other words, accepts_nested_attributes_for is inadequate.

What we’ve tried and looked into as a DRY way to using Rails in large part as JSON service provider:

  • RABL: provides an way to do json views (to replace sending options into as_json/to_json) does not handle incoming JSON to be persisted in a similar way.
  • ActiveModel::Serializers available now and coming in Rails 4 - similar to RABL in that it does not map incoming JSON to be persisted.
  • strong_parameters available now and coming in Rails 4 - keeps you from being able to accidentally persist something that the controller doesn’t specifically define, but does not define JSON view.
  • roar-rails - provides a way to specify both the JSON view and what is accepted, so we are attempting to integrate it currently.

Where complication rears its ugly head:

When you see things like this, it looks easy:

def index
  @companies = Company.all
  respond_with @companies
end

But respond_with makes assumptions about what should be called, and then you should handle errors because it should try to return those as JSON with an appropriate HTTP status code (:ok, :unprocessable_entity, :created, :forbidden, :internal_server_error, etc.), then there is location url which I’m not sure if has a place in a service meant for consumption by a service meant to be consumed by a javascript app?, etc. For an idea of the various things that people have to do and what they run into:

https://github.com/nesquena/rabl/issues/88

https://github.com/rails/rails/issues/2798 etc.

So you maybe end up with something like this just to handle a POST:

this makes sense for Rails served view, as a failed create, but does it makes sense in a JSON service-oriented controller serving to a page served by a different controller? We don’t need to retain state in that case, so new is never called on its own/doesn’t need to be separated out?

def new @company = Company.new respond_with @company end

I have not tested this- just a possibility of something that would use roar-rails which provides consume! and deserialization.

def create # another method to implement that relies on proper authorization if can_create? respond_with(errors: [‘Access denied to create #{self.class.name}’], status: forbidden) end begin @company = Company.new(params[:company]) consume! @company if @company.errors respond_with(errors: [@company.errors], location: users_url, status: unprocessable_entity) else respond_with(@company, location: users_url, status: created) end rescue puts $!.inspect, $@ # TODO: add support for other formats respond_to do |format| format.json { render json: {errors: [$!.message]}, status: (:internal_server_error) } end end end

If you were writing a more generic REST-ish (not necessarily a hypertext/hypermedia driven app) controller that could be used to make everything I described as easy and DRY as possible, such that the Javascript app writer hardly had to think about Rails at all, and providing robust services was mostly just a matter of writing some models and JSON representations for various views, how would you do it?

I am guessing the standard response is “these things depend on the environment, so it doesn’t make sense to abstract them into a controller that will just add another layer of things in the way”, but I’m really trying to provide something that will help here; we have a ton of legacy models, etc. that are currently handled by another SOA system that we need to replace in piecemeal over time, so what may seem like minor differences in the amount of code required will make a big difference for us when it comes time to us having to upgrade Rails, etc.

Sorry I overlooked the rescue block (and no idea how I got parens around the symbol there- must have been copy/paste from something wierd)- may look more like:

rescue
  puts $!.inspect, $@
  respond_with(errors: [$!.message], status: :internal_server_error)
end

Ugh. Didn’t need the array around @company.errors either.

respond_with(errors: @company.errors, location: users_url, status: unprocessable_entity)

Sorry, a lot of status symbols missing the preceding colon. Also, I think the location only needs to be set for HTTP status codes 201 (:created) and 202 (:accepted) according to Leonard, Richardson (2007)- RESTful Web Services. Sebastopol: O’Reilly. pp. 228–230. ISBN 978-0-596-52926-0. (well- if wikipedia is correct, at least). Massive code fail.

Ok, this was pretty wrong. Got off on the wrong foot by looking at this and underestimating respond_with: http://apidock.com/rails/ActionController/MimeResponds/respond_with It’s better documented here:

And the docs are getting better between rails 3 and 4:

rails 3: https://github.com/rails/rails/blob/3-2-rel/actionpack/lib/action_controller/metal/mime_responds.rb master/rails 4: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/mime_responds.rb

In the process, I started writing this (don’t ask- pretty lame but maybe it will continue): https://github.com/garysweaver/convenient-actionpack