Joy of Elixir

17. Mix dependencies

In this 2nd chapter about Mix, we're going to be looking at how to bring in code written by other people into our project. While we can rely on a lot of what we need being a part of what Elixir provides, it is impossible for Elixir to provide absolutely everything that we need. That would make Elixir absolutely huge!

To get around this problem, Elixir has support for bringing in other people's code through a system called packaging. Different packages -- collections of code -- can be found on Hex, a site that aggregates all the packages for Elixir. We can use Hex to pull in packages into our application. We're going to do exactly that in this chapter, pulling in a package called plug_cowboy, which will allow us to start our very own web server. It won't do very much, but it'll be a start! When we get this server up and running, we will be able to open a browser like Google Chrome, type http://localhost:4000 into the address bar, and see something that we created ourselves.

After we've got a simple web server running, we'll introduce a concept called a router that will allow us to send different responses, depending on the path that we type into the browser.

Adding a dependency

To start building this web server, we're going to need to add a dependency on a package to our Mix project. If you think back a chapter, you might remember that we have a deps function within the mix.exs file:

defp deps do
  [
    # {:dep_from_hexpm, "~> 0.3.0"},
    # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
  ]
end

This function is where we define the package dependencies that our Mix project has. In order to create a web server in Elixir, we will use a package called plug_cowboy. Let's add this package to our mix.exs file now:

defp deps do
  [
    {:plug_cowboy, "2.4.1"}
  ]
end

This line inside of deps says to Mix that our project will depend on a package called plug_cowboy, and the second element in the tuple tells Mix the version of plug_cowboy that we want to use is the "2.4.1" version. This is the latest at the time of writing here.

To download this package's code, we need to go to our terminal and then run a Mix task called deps.get:

$ mix deps.get

The output of this command will be surprising:

Resolving Hex dependencies...
Dependency resolution completed:
Unchanged:
cowboy 2.8.0
cowboy_telemetry 0.3.1
cowlib 2.9.1
mime 1.5.0
plug 1.11.0
plug_cowboy 2.4.1
plug_crypto 1.2.0
ranch 1.7.1
telemetry 0.4.2
All dependencies are up to date

"What is cowboy_telemetry?, What about cowlib?", Izzy asks frantically. This was more than we all bargained for. This was only supposed to get plug_cowboy! What's happened here is that mix deps.get has pulled down nine new dependencies, instead of one. This has happened because when we specify a dependency in mix.exs, we are also telling Mix to grab all of that dependency's dependencies, and their dependencies and so on.

A great visualization of these dependencies can be found with another Mix task. This time, the task we want to run is deps.tree. Let's run that now:

$ mix deps.tree

This Mix task will show us the dependency tree for our project:

people
  └── plug_cowboy ~> 2.0 (Hex package)
      ├── cowboy ~> 2.7 (Hex package)
      │   ├── cowlib ~> 2.9.1 (Hex package)
      │   └── ranch ~> 1.7.1 (Hex package)
      ├── cowboy_telemetry ~> 0.3 (Hex package)
      │   ├── cowboy ~> 2.7 (Hex package)
      │   └── telemetry ~> 0.4 (Hex package)
      ├── plug ~> 1.7 (Hex package)
      │   ├── mime ~> 1.0 (Hex package)
      │   ├── plug_crypto ~> 1.1.1 or ~> 1.2 (Hex package)
      │   └── telemetry ~> 0.4 (Hex package)
      └── telemetry ~> 0.4 (Hex package)

While our project itself specifies a dependency on plug_cowboy, the plug_cowboy package depends on four other packages: cowboy, cowboy_telemetry, plug and telemetry. Out of these, cowboy, cowboy_telemetry and plug have further dependencies on additional packages.

When we specify a dependency on a package in Mix, that package can depend on any number of other packages. This means that code across the Elixir ecosystem can be spread out across several different packages.

This plug_cowboy package that we've brought in will allow us to build small modules, called plugs, that can then be served up through a webserver. The plug_cowboy package is a "joining" of two smaller packages: plug, which provides the standardised plug interface that turns Elixir code into web server responses, and cowboy, which is simply a webserver. Using these together will give us exactly what we want: a webserver that can serve Elixir code.

With plug_cowboy installed, let's now set about using it in our people Mix project.

Building our first plug

What we're aiming to do with this chapter is to make it so that we can go to http://localhost:4000 and see something that our very own Elixir code has generated. To get started with this, we will need to write a new module into the people Mix project. This module will be responsible for defining what we see when we go to http://localhost:4000. We'll create a new file at lib/plug.ex and put this code in it:

defmodule People.Plug do
  import Plug.Conn

  def init(options) do
    # initialize options
    options
  end

  def call(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Hello from the People project!")
  end
end

There are a few new concepts here to be introduced. The first of these is the import keyword, used to import Plug.Conn. When we use import in this way in Elixir, what we're saying is that we want to take all the public functions of Plug.Conn, and bring them into the current module. This is how we're able to call put_resp_content_type and send_resp in the call function of this module, without having defined those functions ourselves.

If we weren't going to use import, we would have to write the call function like this:

defmodule People.Plug do
  def call(conn, _opts) do
    conn
    |> Plug.Conn.put_resp_content_type("text/plain")
    |> Plug.Conn.send_resp(200, "Hello from the People project!")
  end
end

By using import, we bring in those functions from Plug.Conn, and remove the need to specify the module from where those functions are coming from every time we refer to them.

When we make a request to http://localhost:4000, this People.Plug module works out how to respond by following the code in the People.Plug.call/2 function. This function starts out by taking a conn, which will be a Plug.Conn struct containing information about the request. "Conn" is short for "connection", and I for one am quite happy that it's abbreviated.

The next thing that's new, is those functions from Plug.Conn themselves: put_resp_content_type and send_resp.

When we use put_resp_content_type, we're telling the browser that made the request that the type of content that we're sending back is plain text, or in computer parlance "text/plain". This is useful because if we were sending back an image instead, we could send something like "image/jpg" back, and a browser would display that image rather than some text.

The send_resp function takes two arguments, a status and a body. The status informs the browser if the request was successful or not. There are many different statuses, and you can see a big list of them here: httpstatuses.com. (And yes, there is a status just in case you're a teapot -- added as an April Fools' joke.) A "200" status means that the request was successful.

The body argument to send_resp is the content that will be displayed on the page when a request is made to http://localhost:4000.

This plug is designed to take in any request and return the same response for all of those requests. Later on, we'll see how to return different responses depending on the request. For now, let's try running this server.

Starting a server

We've now built our first plug, and it's time to start running the web server. To start the webserver running, we're first going to need to start a new IEx session. Let's run this command to start that up:

iex -S mix

Remeber to run this with the -S mix, otherwise we will not be able to access this next function!

To run our webserver, we'll run this command:

Plug.Cowboy.http People.Plug, []

The Plug.Cowboy.http/2 function takes two arguments:

  1. The module that provides the plug for our webserver
  2. A list of options

The options that are passed here are then passed to the People.Plug.init/1 function. We can ignore this for now, since we are not passing any options.

The Plug.Cowboy.http/2 function then returns a tuple with this strange syntax (or something similar to it, your numbers might be different!):

{:ok, #PID<0.242.0>}

We know from Chapter 11 that tuples can be used to indicate the success or failure of a particular operation. From the output here, we can see that the operation was successful... but what's that second part? That is a PID, indicating an internal Elixir process -- "PID" means "Process identifier". All Elixir code runs inside processes, and the second element for this tuple is telling us that our webserver is running within a particular process, with the identifier of 0.242.0.

We could choose to start up a separate plug using this same function and Elixir wouldn't even start to break a sweat.

Now that our server has started running, let's try and access it by opening a browser (if you're reading this book, chances are you've already got one open...) and go to http://localhost:4000. When you go there, you should see:

Hello from the People project

And that's all you'll see, as that's all we've told Elixir to show us.

If you wanted to change the content that's returned here, you can edit the People.Plug.call/2 function to something else:

def call(conn, _opts) do
  conn
  |> put_resp_content_type("text/plain")
  |> send_resp(200, "Hello again from the People project")
end

And then in the iex -S mix session, you can use the r helper to recompile and reload that module:

iex> r People.Plug
{:reloaded, People.Plug, [People.Plug]}

Once that module has been recompiled and reloaded, going to http://localhost:4000 will show a different message now:

Hello again from the People project

Sending a single response from a web server is a good feat, and you should be proud to have accomplished that already. But what would be cooler is if we could send different responses based on what's requested.

Taking a different route

When you've been reading this book, you might've noticed that the address changes in the browser, depending on what chapter you're reading. This chapter's address is https://joyofelixir.com/17-mix-dependencies and the last chapter's address is https://joyofelixir.com/16-introduction-to-mix/. These addresses are how the joyofelixir.com server knows which chapter you want to read.

We're going to do something similar here by using a part of the plug package called a router. A router tells Plug how to serve particular requests, depending on the address requested.

In this section, we'll use the Plug.Router module to dynamically choose which plug to serve our responses with. When we go to http://localhost:4000/hello/Izzy, the server will respond with "Hello, Izzy!". And if we go to http://localhost:4000/goodbye/Izzy, then the server will respond with "Goodbye, Izzy!".

Let's set this up now by creating a new file called lib/router.ex within the people project:

defmodule People.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "hello/:name", to: People.Hello
  get "goodbye/:name", to: People.Goodbye

  match _ do
    send_resp(conn, 404, "there's nothing here")
  end
end

This file starts out by defining a module called People.Router. This module will be our router for requests coming into our application, and is in charge of determining what plug to route our requests to.

In order to do that routing, we need to use Plug.Router. This use Plug.Router line gives us access to the plug, get and match functions that are used here.

The two uses of plug here define some default behaviour in the form of two distinct plugs. The :match plug makes Plug.Router attempt to match the request path to one of the routes we have defined. The :dispatch plug is responsible for sending that response back to the browser once we have done that routing.

After the uses of plug, we define two routes by using the get function. When you type in address in your browser window and hit enter, your browser does not simply request https://joyofelixir.com/17-mix-dependencies. The request actually looks like this:

GET /17-mix-dependencies HTTP/1.1

Your browser sends this request (along with some other information) to the joyofelixir.com server, and the server dutifully returns you this chapter.

In our situation, when we go to http://localhost:4000/hello/Izzy, our browser will send a request to the server at localhost:4000, and that request will be:

GET /hello/Izzy HTTP/1.1

A GET request is one that is asking a server to get information about a particular page, and then to return that information. A request to /hello/Izzy will match our first route, get "hello/:name", because the :name part is a dynamic part of our route -- it could be anything.

The same rules apply to our goodbye route. A request like this:

GET /goodbye/Izzy HTTP/1.1

Will match the get "goodbye/:name" route.

The final thing that we have defined in our router is a match block. This block tells the server what to do in case there's no route that matches. Imagine we made a request like this:

GET /greet/Izzy HTTP/1.1

Our server does not have any route that matches this shape, and so it will not know what to do in that case. This match _ means to match anything, and then to return "there's nothing here" if there's a route accessed that is not specified within this file.

Before we can run our router, we will need to define both the People.Hello and People.Goodbye modules. Let's start by defining People.Hello over in lib/hello.ex:

defmodule People.Hello do
    import Plug.Conn

    def init(options), do: options

    def call(conn, _opts) do
      conn
      |> put_resp_content_type("text/plain")
      |> send_resp(200, "Hello from the People project")
    end
  end

And we'll define People.Goodbye over in lib/goodbye.ex:

defmodule People.Goodbye do
  import Plug.Conn

  def init(options), do: options

  def call(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Goodbye from the People project")
  end
end

Let's go ahead and see this new router in action. We can use this router in the same way we used our People.Plug module before: by starting an IEx prompt, and then by using Plug.Cowboy.http. Let's start a fresh IEx prompt now. If we're still inside an IEx session from earlier, we'll need to exit it and start a new one so that our People.Router code is compiled, which will make it available in this session.

iex -S mix

Then inside this prompt we'll start our router:

Plug.Cowboy.http People.Router, []

This will start our server again, and we will be able to go to http://localhost:4000 and see this message appear:

there's nothing here

Oh right! This text is the one coming from our catch-all match block at the bottom of the router. We need to go to one of our routes to see something proper. Let's try: http://localhost:4000/hello/Izzy.

Hello from the People project

Alright! That one works. Let's try http://localhost:4000/goodbye/Izzy.

Goodbye from the People project

Excellent! Both of our plugs are now serving requests from our router. If we wanted to, we could add as many different routes as we wished, all routing to different plugs.

What we haven't talked about yet is that :name thing in the routes. Let's look at that now.

Using parameters from requests

When we've defined our routes in People.Router, we've defined them like this:

get "hello/:name", to: People.Hello
get "goodbye/:name", to: People.Goodbye

We know that the :name means that we can put anything we want after the slash, but what if we wanted to actually do something with whatever name was entered? What if we wanted to add a personal touch here?

The way we can do that is by reading out the :name part, and using it in our code. When a request is made to our application using http://localhost:4000/hello/Izzy, that "Izzy" part is sent as data inside the conn struct, under a key called params. If we were to take a peek inside that conn object, here's what we would see:

%Plug.Conn{
  ...
  params: %{"name" => "Izzy"},
  ...
}

We can grab that "name" key out of params, and then use it in our code by using pattern matching. You haven't forgotten about little ol' pattern matching I hope! We can see a quick example of this by experimenting with the Plug.Conn struct within an iex -S mix session:

iex> conn = %Plug.Conn{params: %{"name" => "Izzy"}}
%Plug.Conn{
  adapter: {Plug.MissingAdapter, :...},
  ...
}

This is a struct which has a key called params. We can get the value for that params out by pattern matching like this:

iex> %{params: params} = conn
iex> params
%{"name" => "Izzy"}

Then to get the name out of the params, we can pattern match again:

iex> %{"name" => name} = params
iex> name
"Izzy"

We can pattern match in multiple stages like this, or we could do it in one big go:

iex> %{params: %{"name" => name}} = conn
iex> name
"Izzy"

We can "reach into" the conn struct in this way and pull out the parameter that we need. Let's apply this knowledge in

defmodule People.Hello do
  import Plug.Conn

  def init(options), do: options

  def call(%Plug.Conn{params: %{"name" => name}} = conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Hello #{name}!")
  end
end

This will now display "Hello" and then whatever the name parameter specified in our request is.

Let's apply the same changes to People.Goodbye too:

defmodule People.Goodbye do
  import Plug.Conn

  def init(options), do: options

  def call(%Plug.Conn{params: %{"name" => name}} = conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Goodbye #{name}!")
  end
end

To update our server with this new code, we will need to recompile and reload these modules by using the r function again:

iex> r People.Hello
iex> r People.Goodbye

Then when we go to http://localhost:4000/hello/Izzy we'll now see:

Hello Izzy!

And the same will happen for the goodbye route too at http://localhost:4000/goodbye/Izzy

Goodbye Izzy!

You can now put anything after /hello or /goodbye and it will use that value in our code. Pretty cool!

Exercises

  • Add a new route that accepts a person's birthday as a parameter in the format of year, month and day, like this: 1987-12-04. Use your Person.age function to return how old this would make the person.