A little about Elixir and Phoenix
Elixir is a functional programming language that runs on top of Erlang. With Elixir, you get modern expressive syntax, concurrency, fault tolerance, performance, and scalability out of the box. As a developer who has used other programming languages mainly OOP, trust me when I say my productivity improved quite a lot with Elixir. Please find more information here.
Phoenix is the go-to web framework for Elixir. It has everything you need to build your next thing. Unlike other web frameworks where you can easily get lost in the codebase, Phoenix applications have everything in the place where you think they should be. Phoenix is just but a bunch of Elixir modules. Find more information here.
Our Use Case
Let’s imagine that company X wants their employees to sign in to their network and has contracted us to build a backend service that runs on top of Freeradius. Freeradius is an open source multi-protocol policy server that is modular, and highly configurable. It can be used to authorize and authenticate users in a network. Let’s imagine that this architecture will use Password Authentication Protocol (PAP) and all we need to implement is the logic to add users to the radius database and the ability to add and remove these users from the network.
We are going to develop this service using Elixir/Phoenix, Ecto, and PostgreSQL. Ecto is a well-built ORM that is very easy to use. We shall use it to generate our migrations, validate external/incoming data, and interact with our tables. Since this is not a “hello world” type of application, I am going to highlight the important parts that will get you started designing a Restful service in Phoenix. If you want to jump ahead, knock yourself out with the code.
Creating Phoenix Application
If you have not yet installed Elixir (which also requires Erlang to be installed) and Phoenix, please follow the guide in the links shared above. We are going to use Mix to bootstrap our application. Mix has tasks that can help you in your day-to-day development. To get a list of what you can use, try running mix help
on your terminal. Okay enough with that and let’s generate our application with the command mix phx.new radiusAPI –no-html –no-assets
. Take your cup of coffee and change the directory to radiusApi when it’s done. Inside the lib folder is where we shall spend doing what we love unless we need migrations and we can achieve that in the /priv/repo/migrations folder. Configuration is also important and let’s look at that next.
Configuring our Application
In Phoenix, or any Mix project, all your configurations are usually done in the config folder. Config module takes care of our configuration and you can learn more here. Just know that if you want to configure how your application is compiled, put your config in config/config.exs
. If you want to configure the application i.e database for prod, put it in the config/runtime.exs.
Usually, we put sensitive information in .env file so let’s do that
export DB_PORT=5432
export DB_USER=radiusapi_dev
export DB_NAME=radiusapi_dev
export DB_PASS=radiusapi_dev
export DB_HOST=localhost
Open the config/dev.exs file and use the System module to get the config values in the environment as illustrated below. By the way, to export these configs, just run source .env
on your terminal.
config :radiusApi, RadiusApi.Repo,
username: System.get_env("DB_USER"),
password: System.get_env("DB_PASS"),
hostname: System.get_env("DB_HOST"),
database: System.get_env("DB_NAME"),
port: System.get_env("DB_PORT"),
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
Once you are done, make sure to run this mix task on your terminal to create our database if it’s not already mix ecto.create
. let’s move on to create our first migration.
Adding Migrations
Almost all web frameworks have a way to keep track of changes made to the database tables. Ecto Migration is the module that will help us manage our migrations. All migrations are kept in /priv/repo/migrations/.
Repo in this case is the name of our repository that is found in /lib/radiusApi/repo.ex.
Like Phoenix which has mix phx.*
tasks, Ecto also comes with helpers that we can run on our terminal. One impressive thing about Ecto is that it will figure out how to rollback your changes without specifying in the down callback
. So let’s generate a user table migration with the command mix ecto.gen.migration create_user
. Okay, a file is generated. let’s have a look at it.
defmodule RadiusApi.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
end
end
Now, let’s create a user table with email (unique), first name, last name, and password (clear text password. I am sure this is not secure)
defmodule RadiusApi.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add :email, :string, null: false
add :firstname, :string, null: false
add :lastname, :string
# since its going to be Password Authentication Protocol (PAP), this password is going to be in clear text
add :password, :string, null: false
timestamps(type: :utc_datetime)
end
create unique_index(:users, [:email])
end
end
use Ecto.Migration
on the above code injects the functionality that we need to write our migrations. As you can see, this is how Elixir is expressive. Note that we also created a unique index on the email column and the name <table_column_index>
will be automatically generated.
Ecto Schema and Changesets
As you may have noticed, our application inside the lib folder has two folders, radiusApi and radiusApi_web. The former is where all our business logic goes and the latter is where our web-facing logic goes. so let’s focus on our schema first.
Ecto Schema helps us to put a face to our tables. It is Ecto’s API to helps us define what we would normally call a model/entity in other OOP languages. In lib/radiusApi/user, create a file called user.ex and add the following logic and we will go through it
defmodule RadiusApi.Users.User do
use Ecto.Schema
import Ecto.Changeset
@mail_regex ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/
@permitted [
:email,
:firstname,
:lastname,
:password
]
@required [
:email,
:firstname,
:password
]
schema "users" do
field :email, :string
field :firstname, :string
field :lastname, :string
field :password, :string
timestamps(type: :utc_datetime)
end
def changeset(t, attr \\ %{}) do
t
|> cast(attr, @permitted)
|> validate_required(@required)
|> validate_format(:email, @mail_regex)
|> unique_constraint(:email)
end
end
Our Schema is defined in the schema callback and it will automatically create a User struct for us. I also defined allowed and required module attributes explicitly to be used in our changeset. timestamps function will generate for us inserted_at and updated_at columns.
If you are keen enough, you will notice that we have a function definition called changeset. Changeset is what allows us to cast and validate our external parameters. we have also used the changeset function to ensure we have the required fields and that the email is correctly formatted. unique_constraint
helps us ensure that we don’t violate the unique constraint and this is also triggered after we hit our database.
Adding Context
Context helps us to group related functionalities together and expose them to our web-facing functionalities. This way, you and I can easily recognize a pattern. Okay continuing with our user functionality, we shall need to have the functionality that operates on the user schema. so inside lib/radiusAPI, create a file called users.ex and define a module like so. For brevity reasons, I have excluded other logic and you can find it in the GitHub code shared above.
defmodule RadiusApi.Users do
import Ecto.Query
alias RadiusApi.Repo
alias RadiusApi.Users.{User}
def create_user(attr) do
%User{}
|> User.changeset(attr)
|> Repo.insert()
|> case do
{:ok, user} ->
user = Repo.preload(user, :user_devices)
{:ok, user}
error ->
error
end
end
def get_user!(id) do
Repo.get!(User, id) |> Repo.preload(:user_devices)
end
def get_by_email(email) do
case Repo.get_by(User, email: email) do
nil -> {:error, "user_not_found"}
user -> {:ok, Repo.preload(user, :user_devices)}
end
end
def list_users() do
# password is cleartext because of PAP protocol
query = from(u in User, select: {u.id, u.email, u.firstname, u.password, u.lastname})
Repo.all(query)
|> Enum.map(&from_tuple/1)
end
def update(%User{} = user, attr) do
user
|> User.changeset(attr)
|> Repo.update()
end
def delete_user(%User{} = user) do
Repo.delete(user)
end
defp from_tuple({id, email, firstname, password, lastname}) do
%User{
id: id,
email: email,
firstname: firstname,
lastname: lastname,
password: password
}
end
end
Ecto.Query will get us started writing queries like below. be aware that we shall get a list of tuples that we need to pass using from_tuple/1 function defined above.
def list_users() do
# password is cleartext because of PAP protocol
query = from(u in User, select: {u.id, u.email, u.firstname, u.password, u.lastname})
Repo.all(query)
|> Enum.map(&from_tuple/1)
end
User Controller and the View
Once we have our business logic ready, let’s work on the public-facing side of things starting with Controllers and Views. Inside the controller, we can define actions that will be triggered when a request hits our router. So, let’s create the user controller inside lib/radiusApi_web/controllers/ and name it user_controller.ex.
defmodule RadiusApiWeb.UserController do
use RadiusApiWeb, :controller
alias RadiusApi.Users
alias RadiusApi.Users.{User}
action_fallback RadiusApiWeb.FallbackController
def create_user(conn, %{"user" => user_params}) do
with {:ok, %User{} = user} <- Users.create_user(user_params) do
conn
|> put_status(:created)
|> render(:show, user: user)
end
end
def get_user(conn, %{"id" => id}) do
user = Users.get_user!(id)
conn
|> put_status(:ok)
|> render(:show, user: user)
end
def get_users(conn, _params) do
users = Users.list_users()
conn
|> put_status(:ok)
|> render(:index, users: users)
end
def update_user(conn, %{"id" => id, "user" => user_params}) do
user = Users.get_user!(id)
with {:ok, user} <- Users.update(user, user_params) do
conn
|> put_status(:ok)
|> render(:show, user: user)
end
end
def delete_user(conn, %{"id" => id}) do
user = Users.get_user!(id)
with {:ok, %User{}} <- Users.delete_user(user) do
conn
|> send_resp(:no_content, "")
end
end
end
use RadiusApiWeb, :controller
import some of the useful modules that we need. each of our actions takes two arguments. first, being the conn struct that has all the information about the incoming request. the second argument is the parameters that are sent together with the request. All functions in the controller must return %Plug.Conn{}
.
action_fallback RadiusApiWeb.FallbackController
as the name suggests is the fallback for when nothing matches in this controller. It’s just a Plug with functionality that knows how to return a valid %Plug.Conn{}
. You can use action_fallback to standardize your response in different formats across the entire application.
By default, phoenix expects us to have a view inside the controllers’ folder called user_json.ex. This is the module that will be triggered by the render function. Since our application returns a JSON formatted response, the name of the file as you guessed it is named user_json.ex. You can change the view to be used in your response like so put_view(json: <YOUR VIEW>)
. anyway, let’s create our user view.
defmodule RadiusApiWeb.UserJSON do
alias RadiusApi.Users.{User}
def index(%{users: users}) do
%{data: for(user <- users, do: data(user))}
end
def show(%{user: %User{} = user}) do
%{data: data(user)}
end
defp data(%User{} = user) do
%{
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
password: user.password
}
end
end
Adding Routes
In Phoenix, Routes are the main hub. They match HTTP requests to our controller actions. Routes define Scopes, Pipelines, and Routes. Scopes helps to group our routes under a common path prefix like /admin. Pipelines are just a group of Plugs that can be attached to a specific scope. all our routes are defined inside lib/radiusApi_web/routes.ex file. In our case, we have a pipeline called :api
that accept JSON as the content type. We also defined a scope called API and attached our :api
pipeline to it. Now inside this scope, let’s define our routes like so
defmodule RadiusApiWeb.Router do
use RadiusApiWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", RadiusApiWeb do
pipe_through :api
post "/users", UserController, :create_user
get "/users", UserController, :get_users
get "/users/:id", UserController, :get_user
put "/users/:id", UserController, :update_user
delete "/users/:id", UserController, :delete_user
end
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:radiusApi, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
import Phoenix.LiveDashboard.Router
scope "/dev" do
pipe_through [:fetch_session, :protect_from_forgery]
live_dashboard "/dashboard", metrics: RadiusApiWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
end
If you are still following along, we can start the application with mix phx.server.
One interesting thing about Elixir/Phoenix is that you can also start the application inside the interactive session called iex like so iex -S mix phx.server
. get the Postman collection from here.
Conclusion
We have not covered other concepts such as Application and how our service is supervised but you can read more here or check it in our codebase /lib/radiusApi/application.ex. All in all, writing an Elixir application is just a bliss.