In this episode we're going to talk about a little bit more complex routing and how you can use this to your advantage when you're designing your Rails applications. I'm sure you're familiar with the resources route. This defines the CRUD for books, which means that you can create books, you can edit books and update them, delete them and view all of the books. And we can see that if we run
rake routes in our terminal, you can see that it generates 8 routes and each of those is exactly what you would expect for a CRUD operation.
What if we wanted to add a publish action to these books. We want to be able to define a method to patch the book to say it's published. What we can do here is use the member action specifier and we can put our patch route inside of that, so inside of our resources books, we can define member actions which operate on an individual book, so this will generate books/id/publish, and that will be the patch request that we specified here. So we don't need to define the
to: option, which normally would be
"books#publish", we don't have to define this, because it's inside of our
resources :books, as well as the member action defines that it should be going to publish just because of the name of the route
By this point, it looks a bit like this:
Rails.application.routes.draw do resources :books do member do patch :publish end end end
If we run
rake routes again, you'll be able to see that the publish action here is now listed at the top, and it's already been defined and named, so that we can access it with the publish_book path in our views, and it defined the publish with the ID of the book, so this will operate on a single book and it will be a patch. So we can do the same thing, and we can have an unpublished action, and that's pretty cool, so we can just define individual operations on our books to modify them individually. And sometimes, you want to operate on a collection, so you want to operate on all of the books. So maybe you have a patch and you want to publish all of the books. Or say that you want to import books. So this way you can run
rake routes again and if you have defined the collection route, you can see there's now the published book, unpublished book, but now we have publish_all_books and import_books path. So these don't have the ID of the record in the URL, but they define an action called publish_all and import that will work on all of the books. This way we can define the import to work for importing a CSV of books into your system or having a box of being able to publish all of those books. This is really handy, because it allows us to cleanly define routes without having to go and add our custom ones and if we want to do publish, we can do
patch "books/:id/publish" to the books#publish action. If we didn't have this member functionality here, we'd have to do it manually, and then when you're reading through these, it's a lot less organized, because here, everything is scoped inside of this resources books block.
Now let's say that you're bookstore is going really well and you're expanding past books into more products like Amazon did. You want to start by modifying your URL's to start moving people over to this new resource that you're going to be serving, which are products, they're not books. But for now, because you're still only selling books, you might start migrating people over by changing the URL, but not changing much of your Rails app, because you're not sure how you want to set this up yet.
The first thing you can do here is to say
resources :books, path: "products" and re runrake routes`, you'll see that your path helpers are still going to be named the same, so those didn't change, the actual URL's behind them did, and now there's /products instead of /books, and the controller is still the books controller, so this path option just changes this one word inside of the URL's it generates and keeps everything else the same, so you can customize this for the users, but without changing much of the Rails code at all. So all of the stuff that you've previously written is still going to be exactly the same except the URL's have slightly changed, so that's pretty cool. As you can see, things are getting a little bit more complicated in our routes now, because we're adding these publish actions that should only be for admins, and they're kind of getting mixed in with this stuff that regular users should be able to see. So regular users should only be able to see a couple of these things from resources :books, but the rest of it should be for admins. So they should be able to create books or products and they should be able to delete them, but not public users.
Well, if you're a devise user, and one of the reasons why I like devise, they provide an option to make your routing scoped based upon who's logged in, so if you use devise, you have access to this authenticated method inside of your routes, and you just specify the model that it's going to operate on, and you can scope your routes inside of this so Rails will see the authenticated method, and that will be dispatched over to devise, devise will take a look at the user's session and see if they're signed in as an admin, and if they are, all of these routes are available, but if not, then these routes will not be seen by that user at all, so that means that you can define a second
resources :books, that is for the public, in these you could include only the index and the show actions, and get rid of new, create, edit, update and delete, so that this show action and the views associated with it are only available to the public so you wouldn't have to even show them access to any of these publish or unpublished routes, they couldn't even do anything if they weren't signed in as an admin, and that's really nifty. I really recommend using the authenticated route, and to make this actually functional, we need to specify the module on this resources. What this does is it defines the resources just like before, except now it expects you to have a books controller inside of app/controllers* and it wants an **admin folder inside of controller, so you'll have to create that, and create a second books_controller, but inside of that books controller, you can have the actions for only admins, so you can protect the routes and the controller nice and cleanly, because then that way, you're handled from all avenues, so these URL's won't even be available if you're logged in as a regular customer or as a guest, and then this books controller, we would simply go in and delete everything here except for :
class BooksController < ApplicationController before_action :set_book, only: [:show] def index @books = Books.all end def show end private def set_book @book = Book.find(params[:id]) end end
Your access to books is really clean and simple, so we know that this is for guest users or customers, and then our other books controller, which I haven't created would just be
class Admin::BooksController < ApplicationController, and same as before, you would add your
before_action to make sure that and admin user is signed in, and then you can have all of your other activity in there. This method of handling routes allows you to organize your responsibilities in two different controllers based upon who's signed in, which I think is really awesome, it allows you to organize your controllers in a way that it's expected, you can jump into an application and see that there's a books controller, there's a user/book_controller, there's an admins/books_controller, and you can see that each one of those is handled a little bit differently so you understand the differences between their responsibilities and you can just kind of imagine them seeing the folder structure, so that's pretty nifty, and something that I think that makes you applications a whole lot more manageable as they tend to grow, so I hope you enjoyed this, I'm sure we'll cover more advanced routing in the future, and I will see you next week.
Transcript written by Miguel