Most web applications support the ability to serve not only a static route but also dynamic routes with a specific pattern. It’s time to see how we can leverage Cowboy to add dynamic routes to our router.
Say we want to add a new route to our application that responds with a custom greeting for a person whose name is dynamic. Let’s update our router to define a handler for a new dynamic route. We can also use this opportunity to move our Root
handler (the init/2
function) to a different module. This makes our code more compliant with the single-responsibility principle, making it easier to follow:
lib/cowboy_example/router.exdefmodule
CowboyExample.Router do
@moduledoc """
This module defines all the routes, params and handlers.
"""
alias CowboyExample.Router.Handlers.{Root, Greet}
@doc """
Returns the list of routes configured by this web server
"""
def routes do
[
{:_, [
{"/", Root, []},
# Add this line
{"/greet/:who", [who: :nonempty], Greet, []}
]}
]
end
end
In the preceding code, we have added a new route that expects a non-empty value for the :who
variable binding. This variable gets bound to a request based on the URL. For example, for a request with the URL "/greet/Luffy"
, the variable bound to :who
will be "Luffy"
, and for a request with the URL "/greet/Zoro"
, it will be "Zoro"
.
Now, let’s define the Root
handler and move the init/2
function from our router to the new handler module. This separates the concerns of defining routes and handling requests:
lib/cowboy_example/router/handlers/root.ex
defmodule CowboyExample.Router.Handlers.Root do
@moduledoc """
This module defines the handler for the root route.
"""
require Logger
@doc """
This function handles the root route, logs the requests
and responds with Hello World as the body
"""
def init(req0, state) do
Logger.info("Received request: #{inspect req0}")
req1 =
:cowboy_req.reply(
200,
%{"content-type" => "text/html"},
"Hello World",
req0
)
{:ok, req1, state}
end
end
Similarly, let’s define the Greet
handler for our new dynamic route. We know that the request has a variable binding corresponding to the:who
key by the time it gets to this handler. Therefore, we can use the :cowboy_req.binding/2
function to access the value of :who
bound to the request:
lib/cowboy_example/router/handlers/greet.ex
defmodule CowboyExample.Router.Handlers.Greet do
@moduledoc """
This module defines the handler for "/greet/:who" route.
"""
require Logger
@doc """
This function handles the "/greet/:who", logs the
requests and responds with Hello `who` as the body
"""
def init(req0, state) do
Logger.info("Received request: #{inspect req0}")
who = :cowboy_req.binding(:who, req0)
req1 =
:cowboy_req.reply(
200,
%{"content-type" => "text/html"},
"Hello #{who}",
req0
)
{:ok, req1, state}
end
end
In the preceding code snippet, we get the value bound to :who
for the request and use it with string interpolation to call "Hello :who"
. Now, we have two valid routes for our web server: the root and the dynamic greet
route.
We can test our updates by restarting the Mix application. That can be done by stopping the HTTP server using Ctrl + C, followed by running mix run --no-halt
again. Now, let’s make a request to test the new route with Elixir
as :who
:
$ curl http://localhost:4040/greet/Elixir
Hello Elixir%
Cowboy offers another way to add dynamic behavior to our routes, and that is by passing query parameters to our URL. Query parameters can be captured by using the :cowboy_req.parse_qs/1
function. This function takes a binding name (:who
in this case) and the request itself. Let’s update our greet
handler to now take a custom query parameter for greeting
that overrides the default "Hello"
greeting, which we can put in a module attribute for better code organization:
lib/cowboy_example/router/handlers/greet.ex
defmodule CowboyExample.Router.Handlers.Greet do
# ..
@default_greeting "Hello"
# ..
def init(req0, state) do
greeting =
# ..
req0
|> :cowboy_req.parse_qs()
|> Enum.into(%{})
|> Map.get("greeting", @default_greeting)
req1 =
:cowboy_req.reply(
200,
%{"content-type" => "text/html"},
"#{greeting} #{who}",
req0
)
{:ok, req1, state}
end
end
We have now updated our greet
handler to use :cowboy.parse_qs/1
to fetch query parameters from the request. We then put those matched parameters into a map and get the value in the map corresponding to the "greeting"
key, with a default of "Hello"
. Now, the greet
route should take a “greeting”
query parameter to update the greeting used to greet someone in the response. We can test our updates again by restarting the application and making a request to test the route with a custom greeting
parameter:
$ curl http://localhost:4040/greet/Elixir\?greeting=Hola
Hola Elixir%
We now have a web server with a fully functional dynamic route. You might have noticed that we didn’t specify any HTTP method while defining the routes. Let us see what happens when we try to make a request to the root with the POST
method:
$ curl -X POST http://localhost:4040/
Hello World%
As you can see in the example, our web server still responded to the POST
request in the same manner as GET
. We don’t want that behavior so, in the next section, we will see how to validate the HTTP method of a request and restrict the root of our application to only respond to GET
requests.