Blog

Uptime monitor in Elixir & Phoenix: Routing and controllers

Before we delve into the topic of data gathering in Elixir/Phoenix, let us focus on another core aspect of our uptime monitor application: routing and controllers.

Table of contents

  1. Routing
  2. What are plugs in the Phoenix Framework?
  3. What are pipelines in Elixir/Phoenix?
  4. The scope macro
  5. Controllers
  6. Other actions
  7. Elixir/Phoenix routing and controls: A word of conclusion

Welcome to the fourth part of the article series where we are creating an Uptime Monitor in Elixir. As we said in the previous article on Elixir Phoenix data models, there was a plan to work with data gathering in this part. However, in my opinion, there is one core aspect we should focus on before. This is Elixir Phoenix routing and controllers.

New to the Elixir/Phoenix uptime monitor series? Start with Elixir Phoenix project setup.

Routing

The Router is a big hub of the Phoenix application, where all the requests are dispatched to the right controllers. We had a brief look into the Router while creating authentication and authorization in Elixir/Phoenix application. Let’s take a look at it once again.

In Elixir/Phoenix apps, the Router is a big hub where all the requests are dispatched to the right controllers

The first line in the EMeterWeb.Router module, which is use EMeterWeb, :router, simply makes Phoenix routing functions available in this module. It runs a macro from the >router function in the EMeterWeb module.

The next line - import EMeterWeb.UserAuth, includes authorization functions: redirect_if_user_is_authenticated/2 and require_authenticated_user/2 provided by mix phx.gen.auth in the EMeterWeb.UserAuth module. Those functions are used as plugs further in the module.

defmodule EMeterWeb.Router do
 use EMeterWeb, :router

 import EMeterWeb.UserAuth

...

Going to the next line, we can spot a pipeline macro. To understand what pipelines are we need to discuss plugs first.

defmodule EMeterWeb.Router do

...

pipeline :browser do
   plug :accepts, ["html"]
   plug :fetch_session
   plug :fetch_flash
   plug :protect_from_forgery
   plug :put_secure_browser_headers
   plug :fetch_current_user
 end

 pipeline :api do
   plug :accepts, ["json"]
 end

...

What are plugs in the Phoenix Framework?

Plugs are the heart of the Phoenix Framework. A plug’s task is to operate on connection, check or modify it and pass the unified, modified, and checked connection further into the application. The idea behind plugs is to unify the connection we are working with at all the stages of the request lifecycle.

The whole Phoenix project is built on top of plugs. We interact with them at every stage of the request lifecycle. Also, all the core components, like Endpoint, Router and Controller, are just plugs after all.

Plugs might be implemented in two ways: function plugs and module plugs. In our project we are currently working with function plugs only. I highly encourage you to dig through the Plug’s documentation and read more about the difference between them. Function plugs we are using are just functions receiving and returning a connection.

What are pipelines in Elixir/Phoenix?

Now, as we know what plugs are, let’s go back to the pipeline macro. pipeline defines a series of them, launched one after another. They can be attached to specific scopes in the routes, where they will be launched via pipe_through. It happens often that routes are piped through multiple pipelines.

In our EMeterWeb.Router module we have currently got two pipelines - :browser and :api.

The first one has six plugs:

  • :accepts, [“html”] which is responsible for accepting given request format,
  • :fetch_session, which as the name says, fetches the session data and makes it available in the connection,
  • :fetch_flash, which fetches the session data and makes it available in the connection,
  • :protect_from_forgery and :put_secure_browser_headers, which are responsible for securing the application from cross-site request forgery,
  • :fetch_current_use, which gets the current user and puts them into connection. This one comes from the mix phx.gen.auth we used in the second article.

The :api pipeline has only one plug at the moment - :accepts, [“json”], which accepts only json-formatted requests.

The scope macro

Finally, we came to the scope macro. scope is the way of grouping given routes under a common path prefix. We want to scope things of common functionality, like admin or API routes. We have quite a lot of scopes already. These were mostly generated via mix phx.gen.auth and are responsible for user authentication and authorization.

defmodule EMeterWeb.Router do

...

scope "/", EMeterWeb do
   pipe_through :browser

   get "/", PageController, :index
 end

...

 scope "/", EMeterWeb do
   pipe_through [:browser]

   delete "/users/log_out", UserSessionController, :delete
   get "/users/confirm", UserConfirmationController, :new
   post "/users/confirm", UserConfirmationController, :create
   get "/users/confirm/:token", UserConfirmationController, :confirm
 end
end

With that kinda longish introduction to routing in Phoenix, let’s create our first routes. We will use yet another macro, which is resources. Its functionality is relatively easy. It creates a basic CRUD for a given resource. Navigate to the scope, which requires the user to be authenticated. Then add a line with the resources macro, which takes three arguments: name of the resource, its controller and options. We won’t cover the editing of our model in this tutorial. This is why there is an except option, which excludes those endpoints from generating in our router.

defmodule EMeterWeb.Router do

...

scope "/", EMeterWeb do
   pipe_through [:browser, :require_authenticated_user]

   resources "/sites", SitesController, except: [:edit, :update]

...

Let’s run the min phx.routesEMeterWeb.Router command and check the results.

sites_path  GET     /sites                                EMeterWeb.SitesController :index
sites_path  GET     /sites/new                            EMeterWeb.SitesController :new
sites_path  GET     /sites/:id                            EMeterWeb.SitesController :show
sites_path  POST    /sites                                EMeterWeb.SitesController :create
sites_path  DELETE  /sites/:id                            EMeterWeb.SitesController :delete

As you can see the endpoints for basic model management were created.

Controllers

Controllers are the places our router is pointing to. They are used to group common functionality in the same module. Controllers contain actions, which are just functions receiving connection and request parameters.

We already named our controller in the router. Now let’s create the module file in the apps/e_meter_web/lib/e_meter_web/controllers folder. Name it sites_controller.ex.

defmodule EMeterWeb.SitesController do
 use EMeterWeb, :controller

 def index(conn, _params) do
   render(conn, "index.html")
 end
end

The first line of the module says that it should use our EMeterWeb controller function, which defines all needed uses, imports and aliases. It is a default in our application, all the controllers are using it.

Then we define the first action of our controller - index/2. The index/2 purpose in our case is to return all sites of a given user. Let’s also create show/2 and new/2 actions. The first one should show the site with a given id and the second one should render the new form for the site. In the show/2 we are matching id in params. id is a route param related to this endpoint, which is /sites/:id, and it indicates which site should we fetch and show to the user.

defmodule EMeterWeb.SitesController do

...

 def show(conn, %{"id" => _id}) do
   render(conn, "show.html")
 end

 def new(conn, _params) do
   render(conn, "new.html")
 end
end

For now, we will leave the data loading alone and will focus on rendering those pages. To do so, we will need a view module for this controller. Let’s navigate to the apps/e_meter_web/lib/e_meter_web/view folder and create sites_view.ex.

defmodule EMeterWeb.SitesView do
 use EMeterWeb, :view
end

As it goes with controllers - views also have a standard pack of uses, imports and assigns, which are defined in EMeterWeb under the view function.

The next thing is to add the templates themselves. To do so, navigate to the apps/e_meter_web/lib/e_meter_web/templates folder. Create a new folder called sites. In this folder, create three empty files: index.html.eex, show.html.eex and new.html.eex. EEx is an Elixir templates system, which Phoenix makes use of.

Now - launch the server with the iex -S mix phx.server command. Sign up & sign in, then call the /sites endpoint of our application. You should see an empty page, which is expected behavior, because our index.html.eex template is currently empty.

Now, when we have our templates rendered, we can start filling them with content.

Firstly - let’s cover creating a new site. What we want to deliver to our template to build a form is a website changeset. It is relatively simple. Just alias the Website module at the top of our controller to keep the calls shorter and then, in new/2 call the changeset with an empty Website structure. Then pass the result to the template as the keyword list. Simple as that.

defmodule EMeterWeb.SitesController do
 use EMeterWeb, :controller

 alias EMeter.Sites.Website

 ...

 def new(conn, _params) do
   changeset = Website.changeset(%Website{})
   render(conn, "new.html", changeset: changeset)
 end
end

We can inspect the passed changeset in our template now. Update the new.html.eex template and add inspect inside. Then call the /sites/new endpoint.

<% IO.inspect @changeset %>

We should see the whole changeset in the terminal.

#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    user_id: {"can't be blank", [validation: :required]},
    url: {"can't be blank", [validation: :required]}
  ],
  data: #EMeter.Sites.Website<>,
  valid?: false
>

This is great! We’ve just passed our first data to the template. Now we have to make use of it. We need to create a form for creating the websites in our application. Of course, Phoenix has helpers to make it easier, so we don’t have to write a plain html.

<%= form_for @changeset, Routes.sites_path(@conn, :create), fn f -> %>
 <%= text_input f, :url, placeholder: "Website url" %>
 <%= error_tag f, :url %>

 <%= submit "Sumbit" %>
<% end %>

Knowing that our website model has only one field, url, we can create a form with a single text input and a “submit” button. form_for/4 takes form data, action, options and function arguments. Once we re-render our site, we should see a small form containing our input.

You can also spot the error_tag. This one renders errors from the changeset correlated to a given input.

The question is: why isn’t it rendered with the new form, although there are errors in the changeset? This is correlated to the action field of our changeset. Once it changes from nil to other with some action being performed on that changeset - the form will know that something was made with a given changeset and the errors should be displayed.

We defined action as Routes.sites_path(@conn, :create), yet we didn’t define such action in our controller. To do so, go back to the controller and define create/2 action.

defmodule EMeterWeb.SitesController do
 ...

 def new(conn, _params) do
   changeset = Website.changeset(%Website{})
   render(conn, "new.html", changeset: changeset)
 end

 def create(conn, %{"website" => params}) do
   IO.inspect conn.assigns
   IO.inspect params
   redirect(conn, to: Routes.sites_path(conn, :index))
 end
end

For now, its purpose is only to inspect assignments and parameters and then - redirect to the index path. Refresh the form, fill it and submit. You should see something like this in your console.

%{
  current_user: #EMeter.Accounts.User<
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    confirmed_at: nil,
    email: "kkowalczykowski@prograils.com", 
    hashed_password: "$2b$12$5hp4QP0rSnY7pwf3.Ox6WOKtTqg3PjtaH8mUPBnE8Ec1IWjERXbXm",
    id: "2e8dc36a-1c45-4f2f-89e1-f3ca8a100fac",
    inserted_at: ~N[2021-08-27 10:46:50],
    updated_at: ~N[2021-08-27 10:46:50],
    ...
  >
}
%{"url" => "https://prograils.com/"}

As you can see, we have all the data needed to create a website compatible with our Website model. Let’s create a function for it in the EMeter.Sites context. We will take params, pass them through a Website.changeset/2, and insert the result to the database.

defmodule EMeter.Sites do
 alias EMeter.Sites.Website

 @repo Postgres.Repo

 def create_website(params) do
   %Website{}
   |> Website.changeset(params)
   |> @repo.insert()
 end
end

Now we can call this function in our controller. We will also make a reaction flow using a case statement. The database insertion can return two tuples: {:ok, result} in case of success and {:error, reason} in case something went wrong. We will match those results in our controller and react to them properly.

defmodule EMeterWeb.SitesController do
 use EMeterWeb, :controller

 alias EMeter.Sites

 ...

 def create(conn, %{"website" => params}) do
   case Sites.create_website(params) do
     {:ok, website} ->
       redirect(conn, to: Routes.sites_path(conn, :show, website.id))
     {:error, changeset} ->
       render(conn, "new.html", changeset: changeset)
   end
 end
end

There is only one thing left. Our model needs a user ID to be created.

This is why we inspected conn.assigns before. There is a current_user assigned by the fetch_current_user plug. We add the ID to params and we should be able to create our first Website.

defmodule EMeterWeb.SitesController do
 ...

 def create(%{assigns: %{current_user: %{id: user_id}}} = conn, %{"website" => params}) do
   params
   |> Map.put("user_id", user_id)
   |> Sites.create_website()
   |> case do
     {:ok, website} ->
       redirect(conn, to: Routes.sites_path(conn, :show, website.id))
     {:error, changeset} ->
       render(conn, "new.html", changeset: changeset)
   end
 end
end

Other actions

You should be able to create the logic of other actions in our controller. To keep this article shorter, I won’t discuss all of them, but I will paste my code below with some high-level concepts and explanations, so you have the material to analyze and learn from.

sites_controller.ex - EMeterWeb.SitesController

In the index/2, we just added fetching of websites belonging to a current user. In the show/2, we are trying to fetch a website with a given id. In case of success, we display it, otherwise, we send a 404 and redirect to the index. In delete/2, we are trying to delete a user’s website. If this fails we are raising an error. We also added some statuses and flash messages in various actions.

defmodule EMeterWeb.SitesController do
 use EMeterWeb, :controller

 alias EMeter.Sites
 alias EMeter.Sites.Website

 def index(%{assigns: %{current_user: %{id: user_id}}} = conn, _params) do
   websites = Sites.get_user_websites(user_id)
   render(conn, "index.html", websites: websites)
 end

 def show(%{assigns: %{current_user: %{id: user_id}}} = conn, %{"id" => website_id}) do
   case Sites.get_user_website(website_id, user_id) do
     nil ->
       conn
       |> put_flash(:error, "Website not found")
       |> put_status(:not_found)
       |> redirect(to: Routes.sites_path(conn, :index))

     website ->
       render(conn, "show.html", website: website)
   end
 end

 def new(conn, _params) do
   changeset = Website.changeset(%Website{})
   render(conn, "new.html", changeset: changeset)
 end

 def create(%{assigns: %{current_user: %{id: user_id}}} = conn, %{"website" => params}) do
   params
   |> Map.put("user_id", user_id)
   |> Sites.create_website()
   |> case do
     {:ok, website} ->
       conn
       |> put_flash(:info, "Success, website created")
       |> put_status(:created)
       |> redirect(to: Routes.sites_path(conn, :show, website.id))

     {:error, changeset} ->
       conn
       |> put_flash(:error, "Error, invalid data")
       |> put_status(:unprocessable_entity)
       |> render("new.html", changeset: changeset)
   end
 end

 def delete(%{assigns: %{current_user: %{id: user_id}}} = conn, %{"id" => id}) do
   Sites.delete_website!(id, user_id)

   redirect(conn, to: Routes.sites_path(conn, :index))
 end
end


sites.ex - EMeter.Sites

We added three basic functions:
get_user_websites/1, which takes user ID as an argument and returns his websites,
get_user_website/2, which takes website ID and user ID and gets a website matching both of them,
delete_website/2, which takes website ID and user ID and deletes a matching website.

defmodule EMeter.Sites do
 import Ecto.Query

 alias EMeter.Sites.Website

 @repo Postgres.Repo

 def create_website(params) do
   %Website{}
   |> Website.changeset(params)
   |> @repo.insert()
 end

 def get_user_websites(user_id) do
   Website
   |> where([website], website.user_id == ^user_id)
   |> @repo.all()
 end

 def get_user_website(website_id, user_id) do
   Website
   |> where([website], website.id == ^website_id)
   |> where([website], website.user_id == ^user_id)
   |> @repo.one()
 end

 def delete_website!(id, user_id) do
   Website
   |> where([website], website.id == ^id)
   |> where([website], website.user_id == ^user_id)
   |> @repo.delete_all()
   |> case do
     {1, _} -> :ok
   end
 end
end

Elixir/Phoenix routing and controls: A word of conclusion

In this article, we have learned about routing and controllers in the Phoenix framework.

Our application is starting to have its core functionality implemented. We are on a good way to start thinking about background jobs and collecting all the data of our Elixir Phoenix app. We are going to focus on it in the next article.

For now, I highly encourage you to dig a bit into Phoenix documentation to learn more about forms, HTML helpers and other really useful stuff that makes developing an application much easier.

Any kind of feedback appreciated!

Check our latest product - it's based on our experience of managing over 50-people strong company. The tool we're missing as a small company and not an enterprise.

humadroid.io is an employee and performance management software. It's an unique tool allowing everyone to be in the loop - by having up to date info about co-workers, time-off, benefits, assets, helping with one-on-ones, being a go-to place for company-wide announcements.

Check out humadroid.io
Top

Contact us

* Required fields

The controller of your personal data provided via this contact form is Prograils sp. z o.o., with a registered seat at Sczanieckiej 9A/10, 60-215 Poznań. Your personal data will be processed in order to respond to your inquiries and for our marketing purposes (e.g. when you ask us for our post-development, maintenance or ad hoc engagements for your app). You have the rights to: access your personal data, rectify or erase your personal data, restrict the processing of your personal data, data portability and to object to the processing of your personal data. Learn more.

Notice

We do not track you online. We use only session cookies and anonymous identifiers for the purposes specified in the cookie policy. No third-party trackers.

I understand
Elo Mordo!Elo Mordo!