You already know how to create a static request, which is where you create a
page that doesn't take any parameters and simply renders a view. For example:
localhost:3000/about
. For Rails to process this request, the routes.rb
file
contains a route such as:
get 'about', to: "static#about"
This is mapped to the static
controller and its about
action, which renders
the about.html.erb
view template.
According to REST, if we want to retrieve the post with an id of 42
, we should
navigate to /posts/42
. It's technically possible to create a new line in your
routes file for each post... but that would quickly get ridiculous. You would
have to modify your web server every time someone posts! Enter: dynamic routes.
A breakdown of the dynamic route process flow is below:
-
The
routes.rb
file takes in the request and processes it like normal, except this time it also parses the42
as a parameter and passes it to the posts controller. -
From that point, the controller action that you write will parse the
42
parameter and run a query on the Post model, storing the result in an instance variable. -
Finally, the controller passes the instance variable to the associated view, which renders details of that specific post record for the client.
In review, what's the difference between static and dynamic routes?
-
Static routes render pages that have a hard-coded path connected to them. For example, the
/welcome
path will always show thewelcome
page. -
Dynamic routes will render different data based on the parameters passed to the route. For example, when
42
is passed in as the parameter to the/posts/:id
route, the app should render the data for thepost
with an ID of42
. When222
is passed in, the app should render the data for thepost
with an ID of222
.
In order to setup a dynamic request feature, we will start by writing a test to verify that the page exists:
# spec/features/post_spec.rb
require 'rails_helper'
describe 'navigate' do
before do
@post = Post.create(title: "My Post", description: "My post desc")
end
it 'loads the show page' do
visit "/posts/#{@post.id}"
expect(page.status_code).to eq(200)
end
end
Running rspec
gives us an expected error of: ActionController::RoutingError: No route matches [GET] "/posts/1"
. To correct this error, let's draw a route in
config/routes.rb that maps to a show action in the PostsController:
get 'posts/:id', to: 'posts#show'
Here you will notice something that's different from the static route. The
/:id
tells the routing system that this route can receive a parameter and that
the parameter will be passed to the controller's show action. With this route in
place, let's run our tests again.
You should see a new failure this time: ActionController::RoutingError: uninitialized constant PostsController
. Once we stub out a PostsController
class in app/controllers/posts_controller.rb
, running the tests again will
give us yet another new failure: AbstractController::ActionNotFound: The action 'show' could not be found for PostsController
. This means that we need to
create a corresponding show
action in the PostsController. Let's get this
failure fixed with the code below:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def show
end
end
Run the tests again. Now we see a failure saying that we are missing the
posts/show
view template. Let's fix that by creating a posts
folder in the
views
directory and creating a show.html.erb
file in the new views/posts
directory.
Running the tests now shows that we're all green, which means that the request
will be properly routed through the controller and view and returns an HTTP
status code of 200
.
If you start the Rails server and navigate to /posts/1
or any other post
record, the router will know what you're talking about. However, the controller
still needs to be told what to do with the id
.
Now that we have the routing configured, let's build a test to see if the post
content is rendered on the show page with the title in an h1
tag and the
description in a p
tag. We'll start by creating a test for the title:
# spec/features/post_spec.rb
require 'rails_helper'
describe 'navigate' do
before do
@post = Post.create(title: "My Post", description: "My post desc")
end
it 'loads the show page' do
visit "/posts/#{@post.id}"
expect(page.status_code).to eq(200)
end
it 'shows the title on the show page in an h1 tag' do
visit "/posts/#{@post.id}"
expect(page).to have_css("h1", text: "My Post")
end
end
This gives us a failure that says: expected to find css "h1" with text "My Post" but there were no matches
. We first need to get the ID sent by the user
through the dynamic URL. This variable is passed into the controller in a hash
called params
. Since we named the route :id
, the ID will be the value of the
:id
key, stored in params[:id]
. Let's set that up here:
# app/controllers/posts_controller.rb
def show
@post = Post.find(params[:id])
end
In this line, our show action is running a database query on the Post model that
will return a post with an ID that matches the route parameters. It will store
this record in the @post
instance variable and make it available to the
show.html.erb
file. Let's get our spec passing by placing the post's title on
the show view template:
<!-- app/views/posts/show.html.erb -->
<h1><%= @post.title %></h1>
We're back to green! Now let's implement the description spec:
it 'shows the description on the show page in a p tag' do
visit "/posts/#{@post.id}"
expect(page).to have_css("p", text: "My post desc")
end
This will give us a failure since there are no matches on the template yet. To implement this fix, update the view:
<!-- app/views/posts/show.html.erb -->
<h1><%= @post.title %></h1>
<p><%= @post.description %></p>
Now we're passing again, and you now know how to create dynamic routes in Rails! However, we would be remiss if we didn't follow the full "Red, Green, Refactor" TDD workflow.
There are a few elements of the application that can be refactored. Instead of
the long, drawn out get
route we placed in our routes.rb
file, we can use
Ruby's RESTful defaults and the resources
method. Problem is, we only have one
of the seven RESTful routes. Thankfully, we can pass in an only
option,
selecting which of the seven RESTful routes we care about. In this case, we only
care about the show
action.
Remove:
get 'posts/:id', to: 'posts#show'
Replace with:
resources :posts, only: :show
We will go into detail on what the resources
method does in a future lesson.
For right now, just know that it deals with the seven key RESTful routes. In
this case, we limited it to only make the show
action available.
Run the tests for a final time, and they should still be passing. Nice work!