- Read Tutorial
- Watch Guide Video
In this deep dive, I want to take a look specifically at the routing engine. We are going to create a separate application that we can experiment with however we want. For example, we can generate different routes, and then walk through how the routing system is completely open to customization.
The routing system is the entry point to your application. It’s the way that the controller can accept data and how it can help with the rest of the application, make decisions on what is rendered to the user. So this guide, we will focus on the routing engine.
I'm especially excited about this deep dive because in Rails the routing system is the entry point for the entire development process. For me personally, Rails started to click when I began to understand how the routing engine worked. After that, everything else seemed to fall into place and that is why I want to impart this understanding to you.
The first step we will take is to create a separate and new application. Open your terminal and navigate to your desktop. Then use the command rails new RoutingApp -T —database=postgresql
Part of the reason I am choosing to use the postgres database in the generation process is that we will be communicating with the database, and I think postgres is faster than SQLite.
Next change directory into your app with cd Routing App
Now we will start with a baseline and use the scaffold to create Blogs
. The command for that is:
rails g scaffold Blog title:string body:text
We've done this process a number of times, and hopefully it is starting to become more familiar to you. This command will generate all of our blog functionality.
Rake Routes Review
Now, if I now type the imperative rake routes
, we can see all the basic routes.
To review, the left-most column is called Prefix
and this is the method we can use inside our code base. The prefix is going to give us our full route path and be our method.
Then we have our Verb
column. This will tell us if we're getting information from the server or if we're sending information to the server, for example, when creating or editing an item.
Next is the URI Pattern
This is a full list of the URL paths for the browser. For example
/blogs/new
is the browser path to create a new item.
Lastly, we have the column that contains the mapped controller action Controller#Action
So far, this is review. However, I wanted to give you a chance revisit these items because during this entire guide, we're going to be coming back and looking at this table quite a bit.
Routes.rb
Let's open the code base in sublime. The file we're going to be using throughout this guide is the routes.rb
file. So, let's open that. If you press command t and start typing ‘routes’ you will be able to open the file very easily.
Rails.application.routes.draw do resources :blogs end
Here in the file we have our resources :blogs
. This is the standard and basic route system.
The next task I want to do is create a controller. I’m going to use a controller generator for pages. The code is:
rails g controller Pages about contact
This generator will create everything we need. If you run rake routes
again, you can see that we now have our new pages added to our list of routes.
Take a look at about
and contact
. They have their own method prefixes to use as a path inside of a file, the URI pattern for the browser path, and controller action.
Customizing the URI Pattern
Notice how the URI pattern indicates the URL will be /pages/about and /pages/contact, let’s customize that.
The first step in the process is to go to routes file and remove the word /pages in both the about
and contact
routes.
- get ‘pages/about’ + get 'about' - get ‘pages/contact’ + get 'contact'
First we will address ‘about’. We want to bypass the /pages
portion of the route, because this is an annoying path to use. It’s rare to leave this default as you usually you want to have a custom route for static pages like this. After the ’about’
we want to add the word to
and then map to the controller using pages#about
. The ‘pages’ portion of this indicates that this is mapped to the pages controller.
- get ‘about’ + get 'about', to: ‘pages#about’
Now, save the file and run rake routes
. It looks like I have an error. The indication is Missing :controller key on routes definition, please check your routes.
Looking back at my routes I see I moved ahead with out mapping my contact page to a controller. Let’s update that:
Rails.application.routes.draw do get 'about', to: ‘pages#about’ get ‘contact’, to: ‘pages#contact’ resources :blogs end
So, if you see that error, it is likely that the get request has not been mapped to it’s controller correctly. Remember, by default, if your route has the name of the controller as the first part of the get
such as
get ‘pages/contact’
then you are not required to use the to:
action. However, if you remove the controller Rails is expecting you to give a mapping so you will need to indicate that with the to: controller#action
like this:
get ‘contact’, to: ‘pages#contact’
Now if we run rake routes
you can see nothing has changed except for the updated URI pattern. Therefore, instead of /pages/about
we simply have /about
, which is really helpful.
Specific Customization
However, that’s only a glimpse of the customization we can do. Let's imagine we have our contact page, but our client came back later and said that they wanted it to be called a lead generation page with a very specific URL. This is a task I've been asked to do multiple times, so I thought it would be useful to show you how.
To do this, we can pass any path we want instead of contact
. For example, you can change the path to literally anything you want.
get 'leadgen/advertising/landingpage/lead', to: 'pages#contact'
Whatever you choose to use here, Rails will generate your route for you. Run rake routes
, in the terminal again and you will see this custom route.
You'll see that we have this crazy long URL. Start the rails server in a new terminal, then go to the browser. We will start at “localhost:3000/blogs” just to verify everything is working.
Here we have an error. I skipped one little step. We need to create our database with the commandrails db:create
It is good to note if you ever see this error message FATAL: database does not exist it simply means you created the app, but didn't create the database, which is a very easy fix.
We also have to run db:migrate
. I was so excited about the routing, that I forgot some of the basic prerequisites to get it working.
Now if you refresh your browser, you'll see everything is working. You can click on the create blog button, and we have all of our CRUD functionality.
Next, if you go to "localhost:3000/about", you'll see that this works too.
Also, "localhost:3000/pages/about" will no longer work.
Now, copy our crazy long /leadgen/advertising/landingpage/lead
URL and paste it in the browser, and that works perfectly as well.
So, you can give any path you want and the Rails routing engine is good at picking that up.
Customizing the Prefix
Now, I have one little issue with leaving some of these defaults. If you look at our routes table in the terminal, you'll see that all the proper prefixes alongside the extra long one we created.
The issue with this is the length of the code it creates. For example, if I want to link to this page from our about
page, I'll have to do something like this:
<h1>Pages#about</h1> <p>Find me in app/views/pages/about.html.erb</p> <%= link_to "Lead page", leadgen/advertising/asdf/lead_path %>
Save the file, and with that in place go to your about
page in the browser . When you click on the "Lead gen" link, and it'll take you to the page you want. However, this is really bad code. I don't want to have such a long method for something so basic. Actually, I want to define a different method, and customize this with lead_path
- <%= link_to "Lead page", leadgen/advertising/asdf/lead_path %> + <%= link_to "Lead page", lead_path %>
I can define that in our routes.rb
file. We need to update our get
request using as:
- get 'leadgen/advertising/asdf/lead', to: 'pages#contact' + get 'leadgen/advertising/asdf/lead', to: 'pages#contact', as: ‘lead'
If you run rake routes
again, you'll see that our prefix is changed, and we have our ‘lead’ method.
If you go to the browser and hit refresh, you will see that the lead page is working. And we have much cleaner looking code in our files. All of that is looking good. Also, we don't have to rely on our controllers to drive the way our application flows. You'll constantly be asked to create pages at different stages of development. If you have to add something after the controller is generated, it is no problem whatsoever.
Creating a New Page
If you notice we do not have a home
page. We are just going to create one from scratch. I think it makes sense to put it in the pages controller.
The first step will be to open the pages_controller.rb file. Next, add a method called home
for our homepage.
class PagesController < ApplicationController def about end def contact end def home end end
Save your file, Then, go to app/views/pages/
and create a new file by right clicking on the directory and giving the new file the name home.html.erb
.
Open the file, and create a heading tag that says homepage. <h1>Homepage</h1>
Root Path
In the routes, I don't want to define a home page route the same way we did for about
. Instead, I want to create a root path
. Though we've done this before, it's good to have a refresher. So, we want to use root to:
followed by the controller mapping, which is this case is ’pages#home’
.
Rails.application.routes.draw do get 'about', to: 'pages#about' get 'leadgen/advertising/asdf/lead', to: 'pages#contact', as: ‘lead’ root to: 'pages#home' end
So, if you go to the browser and simply type "localhost:3000", it'll open your homepage.
If you go to the terminal and run rake routes
, at the very bottom of the table is the root
path and you can see it is mapped to pages#home
This is a really useful tool. Whenever you define root
, it will automatically give you the root method you don't have to define it like you did for about
or contact
. You will simply be able to use root_path
. This is really efficient for setting up a home page.
Nested Routes
The next thing I want to explain is how to create nested routes. We'll start by creating another controller named Dashboard
rails g controller Dashboard main user blog
Note, the names main user blog
can be anything you want, they are just pages we are going to build. Imagine you are creating an application and you need an admin dashboard - that's a fairly common feature you will need to build.
If I open the routes.rb
file, all the dashboard items are listed.
Rails.application.routes.draw do get 'dashboard/main' get 'dashboard/user' get 'dashboard/blog' get 'about', to: 'pages#about' get 'leadgen/advertising/asdf/lead', to: 'pages#contact', as: 'lead' resources :blogs root to: 'pages#home' end
These routes are exactly what the controller thought we wanted. In reality, we might want a different route. These changes happen quite often in the development process. In a real-world scenario it is always possible that you start off with the routing system set up one way, only to discover that a different approach is needed.
Suppose the routes we just created were for an admin dashboard. In effect, it makes sense for the route to be your-website-name/admin/dashboard/main
To make this possible, we're going to use something called nesting
. We will implement namespace
to accomplish that. Here, we're creating a namespace
called admin
, and inside this we're nesting the routes for our dashboard
pages.
namespace :admin do get 'dashboard/main' get 'dashboard/user' get 'dashboard/blog' end
This step alone will not work, and I'll show you why. If you go to "localhost:3000/admin/dashboard/main", it will throw an error, uninitialized constant Admin.
Obviously this is a problem, but what does that mean? Rails has not given us a very straightforward error message. You can see that it has directed us with the word Admin
(note that it begins with a capital). However, if you look at the routes, you will find more answers.
Let's switch to the terminal and run rake routes
. You'll see something interesting, especially with respect to the way the controller action works in relation to nesting.
Our newest items are listed at the top of the table. Take a look at the controller action for the first one. It has been listed as admin/dashboard#main
. As a matter of a fact, all three have that admin/
at the beginning. It is curious that the correct URI pattern was created and our browser is going to be pointed to the right location. Also, our method names are good.
Nesting the Controller
So the issue is in the controller action. With our current setup, Rails can not find our dashboard controller. The way the Rails routing system works, is that when it is doing a lookup and it comes to a name space
it will interpret it in a specific way. Here in our example it is decoding the process like this: I’m expecting to have an admin directory with a dashboard controller inside of it. So, our fix is going to be inside of our controller directory
Navigate to app/controllers
. Right click on the controller directory create a new folder called admin
. Then, move the dashboard
controller inside this admin
folder. You can do this by right clicking on the dashboard_controller and choosing ‘reveal in finder’. Once in finder you can drag your dashboard_controller.rb file into your admin folder.
Now if you go back to sublime you will find your dashboard_controller.rb file inside the admin directory. This is set up properly, but there’s one more little step you need to do, and if you never done it before it's not the most intuitive. In order to implement nesting, and because this controller is inside the admin
folder, the class name has to be informed, and reflect that.
- class DashboardController < ApplicationController + class Admin::DashboardController < ApplicationController
Now the entire setup will work with the controller.
class Admin::DashboardController < ApplicationController def main end def user end def blog end end
The final change we have to make is to update the views. Let’s let the errors in the browser dictate the steps we take. If you refresh it now you'll not you get a different error,
This is a crazy long error message! Don’t let the it scare you away. Rails has some of the best error messages in all the programming languages and frameworks I've ever worked in. If you read through the message, it tells you what's wrong. Admin::DashboardController#main is missing a template for this request format and variant. The key phrase is missing a template, and this indicates that the system can't find the view files.
If you go to views/dashboard
, you can see the files. But remember we moved our controller to a directory called admin
. Consequently, we have to do the same thing for our views
as well. So, create a new directory called admin
in your views
directory, and move the dashboard
folder in to it.
You can do this in finder. Navigate to your RoutingApp. Go to your views and create a new directory by clicking on “New Folder” and giving it the name admin
. Next move your dashboard folder inside of the admin folder. Now everything should work correctly.
If you refresh the browser, everything is working fine.
We now have a fully nested route. If you want to see the blog
or user
page, just use, "localhost:3000/admin/dashboard/blog", and it should work too. Now all of the items inside our admin route are nested properly.
This is an important concept to know, and that's why I want to spend some extra time explaining the process of nesting. In general, if your application becomes even moderately large, you'll have to namespace a number of your routes.
It’s important to remember the complete workflow: Once you have created a nested route using namespace you need to nest your controller class, then nest the controller folder inside of the parent directory. Last you need to place your view files in a parent directory as well.
Revisiting Resources
Returning to our routes.rb file, you have learned how to implement nesting and namespacing
namespace :admin do
. You’ve also learned to create a completely custom hardcoded route
get 'leadgen/advertising/asdf/lead', to: 'pages#contact', as: 'lead'
,
and you've learned how to implement some custom items
get 'about', to: 'pages#about'
.
Now, I want to come back to resources
. One of the questions students have asked is “How do I rebuild resources from scratch?”
For example, let’s say I add something like resources:posts
to the routes.rb file. The first question that might come to your mind is “What in the world are you doing? We don't have a post controller, or even a concept of posts.” The answer is you can actually create anything from scratch. You should not feel like you have to run a generator for every single thing. We can build all of this.
First, we can go into to the terminal and run rake routes
You can see that we now have all of these post
actions. In fact, they're identical to blogs
because we used the resources
item.
However, if you go to the browser and type "localhost:3000/posts", it's going to throw an error.
This is because it's looking for a posts controller. If you do this backwards, in other words if you do not use a generator, and you want to create the functionality from scratch, it's fairly easy to know the naming. The name of the controller is going to be posts
.
The Posts Controller
So, let's create the posts
controller. Right click on the controller directory, choose create new file, and give it the name posts_controller.rb
You have to give it this exact name as the Rails routing system is only going to look for something called posts
. It is important to remember and follow this rule because that is the way Rails works. The parsing engine is running in the background and looking for names that match perfectly.
In the post_controller.rb
file, we're going to create a class called PostsController
that inherits from ApplicationController
.
class PostsController < ApplicationController end
If you open the pages controller, you will see the code is organized the same way. When you run the generator nothing magical happens. All the generator does is speed up the process by writing some of this code for you. Nevertheless, you're always in total control over what's happening.
Now, let’s create an index
action inside this class.
class PostsController < ApplicationController def index end end
Save the file. Now head over to the browser, but before you refresh, take a moment think of what what error we're going to get. Think of what piece we have not implemented yet. Go ahead and refresh now. Did you guess it? You can see this is a different error. This error says that we are missing a template for this request.
This indicates we don't have an associated view file. Looking at the views you will see we do not have a posts directory yet. Right click on the views
folder and create a directory called posts
. Because we have only implemented the index
action in our controller. We only need to create a view for that. The name of the file should be index.html.erb
. We can put whatever we want in this file, as we aren’t limited to only listing out the posts in this file. That is the traditional way this file is used, but we can simply add a heading. <h1>Here are the Posts</h1>
without even having the posts there.
If you save your file and then refresh the browser, you will find everything works correctly. Notice how quickly we were able to rebuild that entire process.
We were able to leverage the resources
method and build everything from scratch. In fact, we basically wired things up backwards, and essentially did a reverse engineering of what the controller generator does for us.
Globbing
So far we've covered quite a bit of ground in regards to routing. We talked about namespacing, custom routes and rebuilding resources completely from scratch.
Next, I'm to talk about something called globbing
. Essentially, globbing allows you to group items and routes together, in a catch all kind of situation.
Let's create a new route specific to posts
. Even though we used resources :posts
, we can add more routes for posts. To do that, we will use get
followed by posts/*missing
. This is the syntax for implementing globbing. We also need to indicate what this is mapped to
. For that we will use posts
, a hashtag #
and a method called missing
get `posts/*missing' , to: 'posts#missing'
Next, let's go to the posts controller and add a new method called missing
.
class PostsController < ApplicationController def index end def missing end end
Last, go to the views and add a new file in the posts directory. Right click and name the new file missing.html.erb
.
Before going any further, let's talk about the goal of globbing. When you use the code posts/*missing
the asterisk indicates that any item after posts/
that does not hit any of the defined posts methods will be mapped to the missing
action. A scenario where you might want to use globbing like this would be that you want to have a special feature in your application where users can access custom links. There is always a possibility for users to type anything they want. This route can be a catch all for those random actions.
It will be helpful to examine how this operates. Open the missing.html.erb
file and add the heading <h1>These are not the posts you're looking for…</h1>
Save the file.
Globbing will allow us to catch all of the oddly routed items. To test the process, go to localhost:3000/posts/asadasd/sfdf/324. I can type any gibberish I want. As long as I started with posts/
, anything after that, which doesn’t have a specified route, will be sent to the globbed route, and the missing
action will be called.
Cautions about Globbing
Now, there are two things you should be aware of when using this option.
First, you should ensure that your glob is not too broad. For example, if you were to get rid of posts
and just use*missing
, and you used get /*missing
then you would be catching everything, and that is probably not what you want. Remember when you use globs, you are essentially using a catch all and if is too broad you will be catching all the routes.
The other thing you need to be cautious of is the positioning of the glob in the routes.rb file. For example, let's create a method called new
inside the posts controller.
class PostsController < ApplicationController def index end def missing end def new end end
We also need to create the the view file in the posts directory. The file should have the name new.html.erb
. Inside this file, place a heading such as <h1>My new post</h1>
, then save. Next, go to the browser, and type "localhost:3000/posts/new", This route displays the heading from our new.html.erb
file.
On the other hand, what happens if we move the globs route before resources
?
resources :blogs get 'posts/*missing', to: 'posts#missing' resources :posts
If you hit refresh now, look at what happens:
This is definitely not what we want! It caught our route, even though that was not the action we were looking for. We do not want our globs to override existing routes. This is why you have to be very careful with the placement of globs with in the routes file. localhost:3000/posts still works, but if you add anything after posts, with the glob in the current position, the system will redirect to the missing action. This could be positive in the sense that it caught a route that you wanted it to, but it could also be negative because it overrode other routes you wanted access to.
In Ruby and the entire Rails ecosystem, the way the routing works is that it starts at the top of the code file and works its way down. For performance reasons, if it finds a route that matches, then it ignores everything else in the file.
In this case, when we passed “localhost:3000/posts/new” in the browser, the system went thru each line of code in the routes.rb file sequentially to find the associated route. When it hit the line get 'posts/*missing', to: 'posts#missing'
, which was our glob, it recognized it as a matching route. We wanted posts/new, but because the glob could be post/anything, the system selected it, and ignored everything else.
So, whenever you are using custom routes, especially glob routes, you need to be careful so your app will behave the way you are expecting.
Custom and Dynamic Routes
The last thing we're going to talk about in this deep dive, which also happens to be one of my favorite features, is how to create a completely custom and dynamic route. I'm going to show you an example of this in a real world scenario, as this is something I've personally built out.
Go to https://rails.devcamp.com, you are going to see something interesting.
This is a guide that is nested inside of a course which is nested inside of a section. This is fairly deep nesting. By default, the Rails routing system should have a URL that looks something like this: ’rails.devcamp/courses/name-of-course/section/name-of-section/guides/name-of-guide’.
Remember, I'm using friendly guides, so the ID will not be displayed. Additionally, my routes to do not follow this format because I have a custom route, one that will look for three dynamic items, run the database query, and render the item in the browser bar. This means, the words "courses", "sections" and "guides" will not be displayed in the URL. Let’s examine how to create that.
Navigate to your routes.rb file. We will use a get
and name it query
. Let's envision that you want users to pass it some type of dynamic query on some data in your application. You can use query/:
and pass whatever you want after the colon. Typically this would be an id
, such as get 'query/:id'
Remember, the id
can be replaced by anything, because id
is completely arbitrary. You could even use /:something
. If you are looking for id
, you can use that, but if you're looking for something else, you can place that here too, really, whatever you want. I am going to use
ruby
get 'query/:something'
The way Rails routing works is that if it sees a colon :
, then it does not look at the word following it. Rather, it simply treats it as a dynamic value. It is going to indicate to the controller that we connect this to You should have the ability to query this item. You should have the ability to pull in whatever the user typed in.
Next we will map this to pages#something
. Take note, this mapping doesn't have to be something
, and can be pages#about
, pages#contact
, or just about anything else. However, it has to be something that is present in the controller. We will just use this:
- get 'query/:something' + get 'query/:something', to: 'pages#something'
To get this working, go to the pages_controller.rb file and create an action called something
.
class PagesController < ApplicationController def about end def contact end def home end def something end end
(Developer’s Note: It would be very helpful to set up your sublime panes either side by side or split in half and have both your routes.rb file and your pages_controller.rb file open at the same time).
With our route set up and our method in the controller, we have the ability to find whatever is typed into the route. Like our example from the Devcamp site, where there was a title or a section name, or something similar to that, we should now have the ability to drag the :something
.
Let's work on our action now. The way to grab values from the page in Rails is to use params
, brackets []
, and pass in the item as a symbol like this:
params [:something]
Take note, a symbol starts with a colon, and is followed by the name. Now, this is where the naming is important. Whatever you have in the query followed by a colon :
, it has to match with what you have inside params
For example, if you pass in the standard :id
, in then it should match like this:
get 'query/:id', to: 'pages#something'
def something params[:id] end
In fact, just to reiterate that there is no connection between query and the controller action, let’s make a small change.
In your routes.rb file change the query to else
- get 'query/:something', to: ‘pages#something' get 'query/:else', to: 'pages#something'
Similarly in the pages_controller, update your params in the something
action
- params[:something] + params[:else]
We will store the params in an instance variable called @else
def something @else = params[:else] end
Next, we need to create a view page for something
. Go to the views/pages
directory and right click to create a file named something.html.erb
. Inside this view file, let's display our instance variable inside of heading tag.
<h1><%= @else %></h1>
That should be all we have to do. Let's open the browser and type "localhost:3000/query/hey".
the page will display it just the way we want
Try a few more different words, gibberish or numbers. Whatever you type, it pulls it in. This is pretty cool, but let’s zero in on what's happening here.
We have a query as a route and we are indicating I want you to grab whatever is typed in the:else
Traditionally, you would see a route that is similar to our blog set up. Go to localhost:3000/blogs. Create a blog and then navigate to the show page for that blog. The URL will indicate /blogs/1
, which is the route.
In our case now, we have created something completely from scratch where we have a route called query
. It is not related to anything else we have in our application. It's just a name we gave the route. We can add a slash and then add any word or number sequence and it will be displayed on our page.
The way the data flow work is that first the routing system finds the word query
in the URL. Next, it maps it to the get ‘query’
route in the routes.rb files. Then it looks at :else
, and understands that you're looking for a parameter. It takes whatever value you type in, after the slash and stores it in its params hash params[:else]
, which is in the pages_controller. From that point, it stores it in the instance variable called @else
, and finally passes the value in this instance variable to the view.
You can extend this even further! Let's say you want to have two parameters. You can store both these parameters in different instance variables. First, update your route by adding /:another_one'
- get 'query/:else', to: 'pages#something' + get 'query/:else/:another_one', to: ‘pages#something'
Then update your something
method in the pages controller with an instance variable called another_one and set it equal to params with a symbol of the same name placed in brackets.
def something @else = params[:else] @another_one = params[:another_one] end
Last we need to update our something.html.erb view file to display our second instance variable.
<h1><%= @else %></h1> <h1><%= @another_one %></h1>
Again, to recap, when a user enters the query URL the route is going to be picked up, the system will look for a matching route called query
that is followed by a /
with some data and another /
and more data. It will pass that data and store the parameters as whatever name we have chosen (here we used else and another_one). These values will be passed to the something
method, where we can store the values as instance variables, and pass the instance variable to our view files.
Let's test this out. Before that, note that the hey
route we typed earlier no longer works.
This error is because the system is now looking for two parameters, so it won't work with just one parameter. If you want to have the flexibility to have one or two parameters, then you have to define them separately.
get 'query/:else/:another_one', to: 'pages#something' get 'query/:else', to: ‘pages#something'
Now, if you refresh the browser, the single parameter will work, but you can also pass two parameters.
We now have the ability to have a completely custom query type of route, where we can pass in any values. This is exactly what I'm doing on the devcamp site. I'm passing random routes and asking my database controller to run these queries. This way, I can bypass the traditional way that Rails configures routes.
This guide covered a lot of content. If any of the topics I explained seem unclear, I would definitely recommend you to go through the guide again. Follow along exactly as I've written everything in the routes file. Explore different ways you can change things and how you can pass data from routes to controllers and to view files. Also, customize it however you want.
I can't stress enough how much it helped me when I was learning Rails to see and understand the way data flow worked and how as a developer you can manipulate the routing system to essentially do anything your application needs to do from a data flow perspective.
Good job! Let me know if you have any questions whatsoever, and I'll see you in the next section.