1

Topic: Getting Started with RESTful Rails

Since DHH's keynote at RailsConf there has been a lot of talk about RESTful rails. I finally decided to get up to speed and start exploring. This is a look at the new scaffold_resource and routing that will be in Rails 1.2.

The Rails application is a sort of "jukebox" or more accurately a list of artists and their albums. But enough about the name, let's get started.

saturn:~ bp$ rails jukebox

saturn:~ bp$ cd jukebox/


Until Rails 1.2 is released we'll need to grab Edge Rails, which can be done with a rake task.


saturn:~/jukebox bp$ rake rails:freeze:edge
[... lots of output ...]
Exported revision 5383.

Of course we will need a database:

saturn:~/jukebox bp$ echo "create database jukebox" | mysql -u root -p
Enter password:

Don't forget to edit config/database.yml appropriately! Then go ahead and start the server.

saturn:~/jukebox bp$ ./script/server
=> Booting Mongrel (use 'script/server webrick' to force WEBrick)

NOTE: Whoa! Default mongrel server. Sweet! (This might have changes a long time ago and I've just noticed. If that's the case please forgive my tardy elatedness.)

Scaffold Time

Halt right there! Didn't you read the first paragraph? There is a new generator in town and his name is scaffold_resource and has he got some fancy tricks. He'll give us RESTful scaffolding along with a migration WITH a table definition all in one fell swoop. Oh, AND some routing love. We get all of that by describing the columns and data types we want (separated by a ":") in the generate command.

saturn:~/jukebox bp$  ./script/generate scaffold_resource Artist name:string
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/artists
      exists  test/functional/
      exists  test/unit/
      create  app/views/artists/index.rhtml
      create  app/views/artists/show.rhtml
      create  app/views/artists/new.rhtml
      create  app/views/artists/edit.rhtml
      create  app/models/artist.rb
      create  app/controllers/artists_controller.rb
      create  test/functional/artists_controller_test.rb
      create  app/helpers/artists_helper.rb
      create  test/unit/artist_test.rb
      create  test/fixtures/artists.yml
      create  db/migrate
      create  db/migrate/001_create_artists.rb
       route  map.resources :artists

NOTE: I was working on this with revision 5378 yesterday (until I got side-tracked by real work, how annoying wink and at that point still needed to edit config/routes.rb manually. It looks as if that is taken care of for you now. Pretty cool to see something evolve while you are using it.

Let's keep both models as simple as possible and only give the Album model a title column and an artist_id for the association.

saturn:~/jukebox bp$ ./script/generate scaffold_resource Album title:string artist_id:integer
[...]

Discography

An artist has_many albums so we will need appropriate associations in each model. Here is what I have in artist.rb and album.rb respectively:

class Artist < ActiveRecord::Base
 
  has_many :albums
 
end

class Album < ActiveRecord::Base
 
  belongs_to :artist
 
end

Add Some Data

We can create the tables by running a db migration.

saturn:~/jukebox bp$ rake db:migrate

And we are ready to start adding to the jukebox. Adding a couple will do.

http://localhost:3000/artists

Once that is done we can start adding albums, but before doing that let's take a quick peak at config/routes.rb. I've deleted everything except the lines added by scaffold_resource and it now looks like this:

ActionController::Routing::Routes.draw do |map|
 
  map.resources :albums

  map.resources :artists

end


Notice that I have removed the line that usually handles the routing for CRUD operations in scaffolding.

map.connect ':controller/:action/:id'

We will have something similar with the two map.resources lines. These give us a couple of things, one of which is named routes (http://wiki.rubyonrails.org/rails/pages/NamedRoutes) and another is convenience methods for those routes. Taking a look at app/views/artists/index.html we see these two lines:

<td><%= link_to 'Show', artist_path(artist) %></td>
<td><%= link_to 'Edit', edit_artist_path(artist) %></td>

artist_path() and edit_artist_path() are the convenience methods for the named routes for artists. Named routes are not new, but there are a bunch more now (link to http://peepcode.com/articles/2006/10/08/restful-rails).

I think these are mighty useful. Instead of doing :controller => 'artist', :action => 'show', :id => artist, we have artist_path(artist). This is especially helpful if you make changes down the road.

Ok, now how about some albums.

http://localhost:3000/albums/new

Enter the title of the album and the _id_ of the artist you want to file it under. This is also different from the regular scaffolding. Foreign key _id columns are not included in the regular scaffolding and they here. Add some albums for each of the artists you entered and we'll get on with associations.

Nesting

It would be nice to only show albums for a certain artist. I mean, I don't wanna see any Britney Spears mixed in with my Willie & Lobo. We can do this with nested routes (in routes.rb):

ActionController::Routing::Routes.draw do |map|

  map.resources :artists do |artist|
    artist.resources :albums
  end

end


Which is like saying that albums are mapped within artists. Now if you go to an album url like before, for example, http://localhost:3000/albums/1, you will get a routing error. However, knowing that album 1 is by artist one, we try this:

UPDATE Thanks to jardeon for pointing out that you need to change app/views/albums.rhtml before this will work with artists other than 1. The edit link needs the artist_id in it:

<%= link_to 'Edit', edit_album_path(params[:artist_id], @album) %>

And now this:

http://localhost:3000/artists/1/albums/1

And there it is! The "show" action for album 1. Ok, how about going to this one?

http://localhost:3000/artists/1/albums/

Hrm.. That didn't work. Why not? The error we get is saying something about a RoutingError around line 14 of app/views/albums/index.rhtml, which is this:

<td><%= link_to 'Edit', edit_album_path(album) %></td>

The reason it isn't working anymore is because of the nesting. What we need to do is specify the artist _and_ the album, like this:

<td><%= link_to 'Edit', edit_album_path(params[:artist_id], album) %></td>

We'll also need to change the other convenience methods to include the artist_id. All the new link_tos in index.rhtml will look like this:

<td><%= link_to 'Show', album_path(params[:artist_id], album) %></td>
<td><%= link_to 'Edit', edit_album_path(params[:artist_id], album) %></td>
<td><%= link_to 'Destroy', album_path(params[:artist_id], album), :confirm => 'Are you sure?', :method => :delete %></td>

Ok, great, now the albums are showing up again, but we are getting all albums when we only want albums for the specified artist. We can do this by changing the index method in albums_controller a bit. Change this (line 5):

@albums = Album.find(:all)

To this:

@albums = Artist.find(params[:artist_id]).albums

And try again

http://localhost:3000/artists/1/albums

Jackpot! We get a listing of albums for artist 1. Too bad the links on this page are now broken. We need to make changes to edit.rhtml, new.rhtml, and show.rhtml just like we did for index.rhtml and include the artist_id in the convenience methods.

That All?

No, I've really only scratched the surface here. If you want to dig in some more, I highly recommend the Restful Rails screencast from peepcode.com. There is also a REST cheat sheet on that page. And there are a ton of blog posts out there. One thing that I'd like to play around with more is the combination of a RESTful service and using ActiveResource to consume it:
http://www.ryandaigle.com/articles/2006 … ce-is-here
http://blog.mauricecodik.com/2006/06/in … ource.html

Oh, and a note on using scaffolding, regular or resource flavored. It's a nice way to get up and running quickly, but don't rely on it. ryanb just posted a nice piece related to this: http://railsforum.com/viewtopic.php?id=509. And check out Amy Hoy's post too: http://www.slash7.com/articles/2005/12/ … caffolding

Lots O Links

DHH's keynote:
http://blog.scribestudio.com/articles/2 … te-address

http://www.ryandaigle.com/articles/2006 … ce-is-here

http://jimonwebgames.com/articles/2006/ … d-say-fucd

http://blog.hasmanythrough.com/articles … ship-model

http://cwilliams.textdriven.com/article … -rails-1-2

http://casperfabricius.com/blog/2006/06 … sconf-dhh/

http://www.xml-blog.com/articles/2006/0 … hh-keynote

http://www.loudthinking.com/arc/000593.html

http://peepcode.com/articles/2006/10/08/restful-rails

Last edited by bp (2006-11-02 11:37:59)

Re: Getting Started with RESTful Rails

Awesome job with the tutorial! The links at the end are a very nice touch too.

Railscasts - Free Ruby on Rails Screencasts

Re: Getting Started with RESTful Rails

Nice work, and good links!

bp wrote:

NOTE: Whoa! Default mongrel server. Sweet! (This might have changes a long time ago and I've just noticed. If that's the case please forgive my tardy elatedness.)

Mongrel's been the default in edge rails for at least a couple of months now, but if you don't install it yourself it will revert to lighttpd/webrick.

vinnie - rails forum admin

4

Re: Getting Started with RESTful Rails

Thanks to both of you.

vin wrote:

Mongrel's been the default in edge rails for at least a couple of months now, but if you don't install it yourself it will revert to lighttpd/webrick.

Hehe. I'd gotten so used to just typing 'mongrel_rails start' I didn't even notice. The only reason I used './script/server' here was because I figured people may not have mongrel installed.

Re: Getting Started with RESTful Rails

I ran into one problem with this tutorial, which straightened itself out by the end.

The examples provided work perfectly if you're creating your first album for your first artist.  In my case, I created two artists, and created one album, attributed to the second artist.  But the URL <localhost:3000/artists/2/albums/1> failed, because it wasn't reading the 2 properly.  Putting in /artists/1/albums/1 showed the correct album for artist two.

Once I edited my url helpers to include params[:artist_id] and edited the show action to: @albums = Artist.find(params[:artist_id]).albums, then it all cleared up and worked as intended.

I'm not sure if it's worthy of a note in the tutorial itself, because I don't know how many people will go and do something strange like I did!

6

Re: Getting Started with RESTful Rails

jardeon wrote:

The examples provided work perfectly if you're creating your first album for your first artist.  In my case, I created two artists, and created one album, attributed to the second artist.  But the URL <localhost:3000/artists/2/albums/1> failed, because it wasn't reading the 2 properly.  Putting in /artists/1/albums/1 showed the correct album for artist two.

Thanks for pointing this out. I'm not really sure what would cause that. The show method in the albums controller doesn't rely on the artist_id, so in theory, it shouldn't matter what you put there. I get the same album if I hit /artists/12/albums/4 or /artists/2/albums/4. In practice I would probably want to load the artist and the album, so it would matter, but I'm not sure why this happened. Out of curiosity what revision of Rails are you using?

Re: Getting Started with RESTful Rails

It's today's revision 5404.

I think I've found the culprit, this is the error I received:

edit_album_url failed to generate from {:artist_id=>"1", :controller=>"albums", :action=>"edit"}, expected: {:controller=>"albums", :action=>"edit"}, diff: {:artist_id=>"1"}

It's necessary to change the edit_album_url(@album) line to
edit_album_url(params[:artist_id], @album)

before that page will work.

Re: Getting Started with RESTful Rails

One of my favorite REST-related articles is Refactoring to REST. I thought I would mention it as I didn't see it in the list of links. Seems the comments have been removed on that post - too bad, some were useful.

Edit: Hmm, seems the link stopped working. Well you can go to scottraymond.net and scroll down.

Last edited by ryanb (2006-11-02 11:35:58)

Railscasts - Free Ruby on Rails Screencasts

9

Re: Getting Started with RESTful Rails

Ah, yep, thanks for catching that! I'll add an update.

10

Re: Getting Started with RESTful Rails

ryanb wrote:

One of my favorite REST-related articles is Refactoring to REST. I thought I would mention it as I didn't see it in the list of links. Seems the comments have been removed on that post - too bad, some were useful.

Edit: Hmm, seems the link stopped working. Well you can go to scottraymond.net and scroll down.

Thanks, I forgot to link that one. A great real-world example!

Re: Getting Started with RESTful Rails

<b> "Jackpot! We get a listing of albums for artist 1. Too bad the links on this page are now broken. We need to make changes to edit.rhtml, new.rhtml, and show.rhtml just like we did for index.rhtml and include the artist_id in the convenience methods." <b> <br>

I should said great Post, really enjoy it and follow up to understand better Restful Applications with Ruby, however this last step still makes me have a hard time, try to edit edit.rhtml, new.rhtml and show.rhtml but seems to mix the artist_id with the regular id.

Once I create a new album the insert put the record into the DB but the show seems to have confusion in the URL put the :id of the Album instead of the :artist_id

Best Regards Dino.

Re: Getting Started with RESTful Rails

# In albums_controller.rb

@albums = Album.find(:all)
    Replaced with:
@albums = Artist.find(params[:artist_id]).albums

When add New Album or Edit existing Album show Error on URL putting :id instead of :artist_id.
Generate Error
ActiveRecord::RecordNotFound in AlbumsController#index
Couldn't find Artist with ID=9
The URL Looks Like:
http://192.168.1.36:3000/artists/9/albums
Instead of:
http://192.168.1.36:3000/artists/4/albums [ To show the List of Albums] or
http://192.168.1.36:3000/artists/4/albums/9 [ To display the currently added/edited album ]

On Click Show session dump:
---
flash: !map:ActionController::Flash::FlashHash
  :notice: Album was successfully created.

I can tell the new Album or the Edit Album successfully update the DB.
This makes me wonder where after the flash[:notice] my application is being redirected to the wrong URL ???

I wonder if @album = Album.new(params[:id]) need to be changed to something else !!!

Re: Getting Started with RESTful Rails

Thanks for the tutorial. I'm still really scratching my head on a few things though...

What is the best way to create a "Administrative" area for your site?? I can't figure this one out. Does it mean I have to put code in every controller just to get an admin layout and views? How do you specify that in the url; the "admin/" part?

What's the point of having a url like /products/32/orders/122? Isn't that a little confusing and this be better: /orders/122? Nested resources don't make sense to me. I can understand using the list view, to show only the related children, but to create, edit or delete seems strange.

Re: Getting Started with RESTful Rails

Hope you don't mind if I answer this one bp.

goodieboy wrote:

What is the best way to create a "Administrative" area for your site?? I can't figure this one out. Does it mean I have to put code in every controller just to get an admin layout and views? How do you specify that in the url; the "admin/" part?

Good question. Normally in a RESTful design, there is no special administration section. Instead, user authentication is added and used to determine if a given user is an admin. If he is, he receives special privileges (access to certain actions). You can use simple if conditions in the view to show certain links for the admins. You can then use a before_filter to determine the user going to the given action has the proper access.

For example, if you create an Albums controller and you only want users to access the list/show actions, you can add a before_filter to the create/update/destroy actions to make sure the logged in user is an administrator. If you want a special layout for the admins, you can do this too by passing a symbol to the layout method so it calls another method to determine the layout (see the example in the link).


goodieboy wrote:

What's the point of having a url like /products/32/orders/122? Isn't that a little confusing and this be better: /orders/122? Nested resources don't make sense to me. I can understand using the list view, to show only the related children, but to create, edit or delete seems strange.

Another good question. The only benefit I see is the "/products/32/orders" url where this will show all orders for the given product. Once you get to specifying two ids it gets a little ridiculous IMO. I'm honestly not a fan of nested resources and prefer to just stick with the simple URLs, but other people like them and that's fine with me. smile

Railscasts - Free Ruby on Rails Screencasts

Re: Getting Started with RESTful Rails

OK thanks for that. I get it, it's different than what I'm used to but I'll give it a try.

Another questions here... I've noticed that when you go to a resource/new location... and then post with errors, the errors are missing... because the for actually does a redirect to "create". How do we handle errors from here??  Not sessions? sad

Thanks again!

- matt

p.s. I just bought the peep code video and it's really great!.

Re: Getting Started with RESTful Rails

Another question!

If you create a named resource route... and you want to redirect to the named route instead of the action name... how is that done?

I've assigned "/login" to the restful_authentication plug-in 'sessions/new' url. But when there is an error, it brings me back to '/sessions' url (it's using redirect_back_or_default).

I just don't want to hard code: "redirect_back_or_default '/login'" into my controller, when that "/Login" reference is being created in routes.rb. Do you know what I mean? Or is that just the way it has to work?

- matt

Re: Getting Started with RESTful Rails

goodieboy wrote:

Another questions here... I've noticed that when you go to a resource/new location... and then post with errors, the errors are missing... because the for actually does a redirect to "create". How do we handle errors from here??  Not sessions? sad

It is common to just render the "new" template instead of redirecting, this way the instance variable containing the validation errors is not lost. You could also use sessions, but this is easier.

def create
  @album = Album.new(params[:album])
  if @album.save
    flash[:notice] = "Successfully saved album!"
    redirect_to :action => 'index'
  else
    render :action => 'new' # render here, not redirect
  end
end

Also, you can display the errors in the form view like this:

# new.rhtml
<%= error_messages_for :album %>

goodieboy wrote:

I've assigned "/login" to the restful_authentication plug-in 'sessions/new' url. But when there is an error, it brings me back to '/sessions' url (it's using redirect_back_or_default).

Try placing it above the other map.connect/map.resource definitions in the routes.rb file. When building the URL, the first possible route that passes the conditions is used.

Railscasts - Free Ruby on Rails Screencasts

Re: Getting Started with RESTful Rails

OK thanks getting somewhere now... but strangely (I'm VERY new to rails and ruby) I can't seem to set an instance variable in the create action, and view it in the new action template. . If I set the instance variable in the new action, the template gets it. How can I set the error for a login (session resource) in the create action, render the new action, and then have the new action template display the error message? The session is not an activerecord model. (but a resource).

- matt

Re: Getting Started with RESTful Rails

goodieboy wrote:

OK thanks getting somewhere now... but strangely (I'm VERY new to rails and ruby) I can't seem to set an instance variable in the create action, and view it in the new action template. . If I set the instance variable in the new action, the template gets it. How can I set the error for a login (session resource) in the create action, render the new action, and then have the new action template display the error message? The session is not an activerecord model. (but a resource).

Hmm, I'm not sure what the problem is, the template should have access to any instance variables defined in the action. For example:

# controller
def new
  @foo = 'bar'
end

def create
  @foo = 'blah'
  render :action => 'new'
end

# in new.rhtml
<%= @foo %>


This should return 'bar' or 'blah' depending upon what action ran.

Railscasts - Free Ruby on Rails Screencasts

20

Re: Getting Started with RESTful Rails

Thanks for following up on these ryan! I've been out of the country for a while and just got back. I'm hoping to dig back into this in the next few days.