Rails: Nested resource scaffold
In my previous post I told you about the resource scaffold. What you’ll be doing a lot is nesting these resources. Ingredients in recipes, comments on posts, options for products. You name it, you nest it!
Since Rails does not automatically nest resources for you, you should do this yourself. This is, with some minor tweaks, really easy to accomplish. In this example I’ll create recipes that have multiple ingredients.
I assume you have Rails 1.2.1 installed for this tutorial to work properly.
First, I create an new rails project named ‘cookbook’. I use an SQLite3 database because it’s easy to do so. You may use any Rails compatible database for this example.
$ mkdir cookbook rails --database sqlite3 cookbook cd cookbook
First I create resource scaffolds for both the Recipe and Ingredient models:
$ ./script/generate scaffold_resource Recipe title:string instructions:text ./script/generate scaffold_resource Ingredient name:string quantity:string
As you can see I did not add a recipe_id to the ingredient model because of the has_many relationship. Add this column to the migration file. You should now be able to migrate your database:
$ rake db:migrate
If you add the recipe_id to the generate script the view for your ingredients will include a field for the recipe_id and that’s not what you want.
Next, make the has_many relationship in your models.
app/models/recipe.rb:
class Recipe < ActiveRecord::Base has_many :ingredients end
app/models/ingredient.rb
class Ingredient < ActiveRecord::Base belongs_to :recipe end
So far, nothing new. Next we check out config/routes.rb:
map.resources :ingredients map.resources :recipes
What we want is to map ingredients as a resource to recipes. Replace these two lines with:
map.resources :recipes do |recipes| recipes.resources :ingredients end
This will give you urls like /recipes/123/ingredients/321
Now we need to make some changes to the ingredients controller. Every ingredient belongs to a recipe. First add the filter:
before_filter(:get_recipe) private def get_recipe @recipe = Recipe.find(params[:recipe_id]) end
This will make sure that every ingredient knows what recipe it belongs to.
In the index method of the ingredient controller, make sure you have this:
@ingredients = @recipe.ingredients.find(:all)
This makes sure you only show ingredients for this recipe, and not all ingredients in the database.
Because we changed the route for ingredients, we need to update all ingredient_url() and ingredient_path() calls in our controller and views. Change all occurrences of
ingredient_url(@ingredient)
and
ingredient_path(@ingredient)
to
ingredient_url(@recipe, @ingredient)
and
ingredient_path(@recipe, @ingredient)
Note: Make sure that you don't replace 'ingredient' with '@ingredient' in your views!
Add a link to the ingredients to your recipe's index.rhtml view.
link_to 'Ingredients', ingredients_path(recipe)
And, at last, make sure the create method of your ingredients controller attaches the ingredient to the right recipe. Make sure the first two lines look like this:
def create @ingredient = @recipe.ingredients.new(params[:ingredient])
You may now start your webserver and check out http://localhost:8000/recipes . Create some recipes and click 'ingredients'. You can now add ingredients for this recipe!
The next step will be customizing each method to suit your own needs.









Good article, we need more information about resources as the api docs are not very informative yet.
I have a question, what the difference between _url and _path ?
And I got a habit to create new child records as scoped from their parents. so the create method could be:
def create
@ingredient = @recipe.ingredients.create(params[:ingredient])
@Ibrahim: the different between _url and _path is that the first constructs a full URL, including you host or domainname. The second merely returns a path like /recipes/123 without domain information.
You second point is a good one. I’ll have to update the article later today. Other methods like edit or destroy should also use a format like this:
@ingredient = @recipe.ingredients.find(:params[:id] to make sure you only access the actual ingredients for a certain recipe.
Good writeup. Having a slight issue with the _path that is being created. I have a Client that has_many Matters. On my show page for my client I built in a form for adding new matters. The form has: matters_path(@client, @matter)) do |f| %> which builds the url /matters/ rather than /clients/1/matters/ which is what I need for the proper save. Any advice?
@Brandon: I would first make sure the regular ‘new’ form for matters works. Next, render the form as a component on the show-view of your client:
< %= render_component :controller => ‘matters’, :action => ‘new’ %>
I thinks that should do the trick. If not, make sure you have made the proper change to config/route.rb
[...] Rails nested resource scaffold [...]
Thanks, very happy to see RESTfull stuff popping up. I’ve been tinkering with this code a bit, and i’ve place the new ingredient form right into the show.rhtml of the recipe… now i’m stuck at trying to display error messages. I tried in the show.rhtml of the recipes views (where I put my form for the ingredient) and in my ingredient controller in the create method I put redirect_to recipe_url(@recipe) but cant seem to pass the errors… any ideas?
You only need the ‘parent’ resource (ie., recipe for ingredient) when you’re doing a list or create. Otherwise, you should be able to access the ‘child’ resource directly through it’s id.
If you need the ‘parent’ elsewhere, such as for generating the _path or _url, you can use the corresponding method on the child. Per your examples, you could have…
ingredient_path(@ingredient.recipe, @ingredient)
Note that this will work only for has_many (not habtm).
You have a typo in this article:
In this method
def get_recipe
@SDG: You’re right about that typo. I’ve also made some other changes that I found made more sense, especially the part where ingredients are found for a give recipe. This seems to be the part where you had trouble.
def index
@ingredients = @recipe.ingredients.find(:all)
end
The find(:all) part makes sure all ingredients are loaded. Let me know if that fixes your problem.
Thank you so much for sharing this information – I was wondering how this was achieved. I’m eternally grateful.
I had to use @ingredient = @recipe.ingredients.build(params[:ingredient]) to get this to work- calling new didn’t add the correct recipe_id to the ingredient.
I agree that nested routes are a thing of beauty, but once nested it seems impossible to ‘un-nest’ a resource.
What I am getting at is that I would like to
a) show the ingredients for one recipe. this works fine.
b) show a list of ingredients covering all recipies.
This latter doesn’t seem possible. Logically, the url would become /ingredients but these can only exist inside recipes (giving /recipes/2/ingredients).
Do you know a way to have both? List all ingredients for one recipe AND list them all for all recipes?
@joost: Just add map.resoureces :ingredients to your config/routes.rb
You’ll have ingredients twice there, but that’s no problem at all. I was amazed by this too when I found out, but it works perfectly.
[...] has another post that concentrates on nested [...]
Just wanted to mention that in the latest Rails trunk, you have to prefix a nested resource with its parent. So ingredient_path(@recipe, @ingredient) will become recipe_ingredient_path(@recipe, @ingredient).
I guess the new stuff in trunk is to address problems like joost was having. that way it separates the calls to all recipes ingredients_path & recipe_ingredients_path(@recipe).
brandon wrote:
I have been working on this same issue today the solution I found was
url_for([@cient.matter, @matter]) do |f| -%>
This generates the expected post URL /client/:client_id/matters so the controller can user the params[client_id] to scope the new matter to the correct client!
hope this helps
oops the code in my comment should be:
url_for([@matter.client, @matter]) do |f|
What if recipes where in books. so you wanted book/:id/recipe/:id/ingredients
How would the route.rb look then – can you double nest or should you just not do this? We have a lot of these situations.
@Brian: Just like you nested ingredients into recipes, you can nest recipes in books. Note that this means that you’ll have to use books_recipe_ingredients_url(@book, @recipe, @ingredient))
You can do this. But if you stuff the same recipies in different books, I wouldn’t nest ‘em.
Good luck with your app.
I am using Rails 1.2.4.
When recipes_controller.rb does NOT exist, the mapping to /recipes/1/ingredients does show the ingredients that belong to recipe 1.
That’s ok!
But when recipes_controller.rb DOES exist, the nested resource (/recipes/1/ingredients) outputs
If I call /recipes/1.xml/ingredients it outputs a routing error, that is, there is no route to that resource.
What am I doing wrong?
Thanks