Adding routes with bindings
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.