Web services, Rails-style

Posted by Jamis on March 27, 2006 @ 11:43 AM

The new REST web-service support in Rails 1.1 makes it so easy to add web-services to your app, you might as well do it earlier, rather than later.

Consider: if you have a page in your app that displays a list of people, it might look something like this, without web-service support:

1
2
3
def list
  @people = Person.find(:all)
end

Here’s the same action, with web-service support baked in:

1
2
3
4
5
6
7
8
def list
  @people = Person.find(:all)

  respond_to do |wants|
    wants.html
    wants.xml { render :xml => @people.to_xml }
  end
end

What that says is, “if the client wants HTML in response to this action, just respond as we would have before, but if the client wants XML, return them the list of people in XML format.” (Rails determines the desired response format from the HTTP Accept header submitted by the client.)

Now, let’s suppose you have an action that adds a new person, optionally creating their company (by name) if it does not already exist. Without web-services, it might look like this:

1
2
3
4
5
6
def add
  @company = Company.find_or_create_by_name(params[:company][:name])
  @person  = @company.people.create(params[:person])

  redirect_to(person_list_url)
end

Here’s the same action, with web-service support baked in:

1
2
3
4
5
6
7
8
9
10
11
def add
  company = params[:person].delete(:company)
  @company = Company.find_or_create_by_name(company[:name])
  @person  = @company.people.create(params[:person])

  respond_to do |wants|
    wants.html { redirect_to(person_list_url) }
    wants.js
    wants.xml  { render :xml => @person.to_xml(:include => @company) }
  end
end

It was simple enough that I also added RJS support here. If the client wants HTML, we just redirect them back to the person list. If they want Javascript (wants.js), then it is an RJS request and we render the RJS template associated with this action. Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also include the person’s company in the rendered XML, so you get something like this:

1
2
3
4
5
6
7
8
9
<person>
  <id>...</id>
  ...
  <company>
    <id>...</id>
    <name>...</name>
    ...
  </company>
</person>

Note, however, the extra bit at the top of that action:

1
2
company = params[:person].delete(:company)
@company = Company.find_or_create_by_name(company[:name])

This is because the incoming XML document (if a web-service request is in process) can only contain a single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):


person[name]=...&person[company][name]=...&...

And, like this (xml-encoded):

1
2
3
4
5
6
<person>
  <name>...</name>
  <company>
    <name>...</name>
  </company>
</person>

In other words, we make the request so that it operates on a single entity—a person. Then, in the action, we extract the company data from the request, find or create the company, and then create the new person with the remaining data.

Note that you can define your own XML parameter parser which would allow you to describe multiple entities in a single request (i.e., by wrapping them all in a single root note), but if you just go with the flow and accept Rails’ defaults, life will be much easier.

Posted in Spotlight

Comments

Have something to add? Click here to leave a comment.

27 Mar 2006

1. Mike said...

Wow, how nifty! I am, admittedly, still learning the stack, but how might this work with authentication for web service clients that (probably) won't be implementing session cookies like a browser?

2. Jamis said...

Mike, I'd recommend using HTTP authentication with this web services model. In fact, that's how Basecamp does it. You can google for "http authentication rails" and find at least a few examples of how to set this up.

3. Mike said...

Thanks Jamis. Basic auth makes me shiver, sending a clear password on each request. However, I suppose higher-level approaches like an expiring api key would be more along the lines of a plugin, as it would have heavy assumptions regarding a pre-existing authentication/acl scheme. And even then, I suppose security couldn't truly be expected unless behind https. Will the existing activewebservice sub-framework be deprecated? Keep up the great work!

4. Alex said...

Jamis, can this REST support (as well as ActionWebService::Client::XmlRpc as documented http://api.rubyonrails.com/classes/ActionWebService/Client/XmlRpc.html) be used for outbound client apps, such as allowing my Rails app to interact with other web services (like eBay, Amazon, Google, etc.)? I was under the impression I might need to use Ruby's XMLRPC::Client library to do what I needed, but if an interface is built into the Rails framework I would love to use it. If you can point me in the right direction I would be very greatful. Thanks so much!

5. Jamis said...

Mike, yah, Basic auth is definitely not secure, but it's got more going for it than a username/password form in a browser that submits via http. The best solution is to protect it all via https, but if that isn't an option you can always implement your own authentication scheme.

6. Jamis said...

Alex, the REST support in Rails 1.1 is only targetted at providing services, not consuming services. If you want your Rails app to consume external services, Rails won't necessarily help you there.

7. Charlie said...

Hi Jamis, Thanks for posting this. If you have a second, take a look at the approach I took to this problem - see http://cfis.savagexi.com/articles/2006/03/23/content-negotiation-and-rails It works by adding HTTP content negotiation into ActiveView. The advantage is that you don't have to add any code to your controller. Thus, I think its a cleaner solution than what is provided in Rails 1.1. Be interested in hearing your thoughts on it. Either way, glad to see this added to Rails.

8. Jamis said...

Charlie, The thing I like about Rails' approach is that it handles things like redirects (which don't use a template, and would only be used when text/html was desired), and also lets you generate the xml programmatically (without a template). In fact, when writing the Basecamp API, there were many instances when I didn't want to render a template in response, but just wanted to specify a status code and return. The "respond_to" approach in Rails 1.1 also lets you cleanly specify behavior that ought to only exist in one kind of response. Moving the magic to the view definitely has some plusses, though. Perhaps it would be possible to collect the best of both worlds into one implementation?

9. Joe Martinez said...

Sweet. Just hopped on the Edge - looking forward to trying this along w/ RJS!
28 Mar 2006

10. Neil Wilson said...

Is there a mechanism baked into Rails to do this sort of stuff in reverse and manage the request response cycle out towards another web service? My head says that the view rendering and controller dispatch systems should really be managing the outbound initiated links as well as the inbound initiated ones. What do you think? NeilW

11. Jamis said...

Neil, nothing in Rails right now for handling outbound REST requests. I don't think that belongs on Rails itself, although it would make a great library, or plugin.
29 Mar 2006

12. BillSaysThis said...

Jamis, "Neil, nothing in Rails right now for handling outbound REST requests. I don’t think that belongs on Rails itself, although it would make a great library, or plugin." Why do you say this? From my perspective, making API consumption easier is pretty important--don't you want to make it simple for me to consume Backpack API in my API (which it would actually fit) and therefore perhaps generate new paying accounts for 37S?

13. Jamis said...

Bill, sure, making API consumption easier would be wonderful. I just happen to believe it is beyond the scope of Rails. Is it so wrong to have to install a 3rd party lib to do the external API thing? Rails is about building web apps. Not about consuming web apps. That doesn't make consuming web apps an unworthy goal. Just a goal that Rails won't likely pursue.
30 Mar 2006

14. BillSaysThis said...

No, using a different component isn't a terrible hardship and something I'm willing to do. I definitely am aware of the 'opinionated' nature of Rails decisionmaking but this just seems like a matching pair and therefore conceptually within the scope.
31 Mar 2006

15. Charlie said...

Hi Jamis, I agree. Content negotation could be used for default cases while resond_to could be used to override the default. Thus, it would mirror how Rails picks a default template but you can override that via render(:template => 'some_template') I've posted some more ideas about it on my blog if you're interested. See http://cfis.savagexi.com/articles/2006/03/31/rails-and-content-negotiation-revisited Thanks - Charlie

16. Ryan Lowe said...

It's not really clear how the "wants" variable gets its value. Are you parsing the header in a before filter somewhere, or is there some other magic going on? David's examples use a variable "type", so I'm guessing the variable name isn't magic. Could someone make it explicit?

17. Ryan Lowe said...

Sorry about that, I misunderstood the Ruby code. A look at the Rails API cleared things up. Hope this helps other people: http://api.rubyonrails.org/classes/ActionController/MimeResponds/InstanceMethods.html#M000061
07 Apr 2006

18. Alex said...

Just wondering, but for wants.js requests would "to_json" be a good bet?

19. Jamis said...

Alex, when a request "wants.js", it means it wants "text/javascript" back. This means you need to send actual Javascript code, and not marshalled data. By default, rails will render an RJS template for "wants.js".
22 Apr 2006

20. jerry said...

By the way, the order in which you check each 'want' is important. IE6[1] appears to ask for text/javascript before text/html even for a normal (address bar) URL. So if you have a setup which degrades gracefully (e.g. wants.js modifies the page while wants.html sends the whole page via the usual template) you must have wants.html before wants.js so that the first page load gets the right response. Thereafter, everything works as expected (in the background). Nice job though. Makes AJAX real easy. [1] I saw it with v6.0.2900.2180.etc.etc
13 Sep 2006

21. etienne.durand@woa.hu said...

I am just trying to figure out how to map this with the api definition. I wrote a question on http://www.ruby-forum.com/topic/80882#new but without any answer. Can anybodyhelp me?