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:
- The module that provides the plug for our webserver
- 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 yourPerson.age
function to return how old this would make the person.