Building a RESTful API Backend with Authentication using Elixir Phoenix

After a couple of years working with RubyOnRails — I’m a huge fan! — I thought it’s time to learn another web framework and after doing some research, I chose Elixir and Phoenix for many reasons such as performance, latency and of course Erlang VM. I reckon Ruby developers will find this powerful framework amazing and easy to learn as the syntax is really close to what Ruby offers.

In this tutorial, we are going to create a REST API backend using Phoenix. We’ll also use some great packages to authenticate users and authorize them to access our APIs.

Prerequisites

elixir -v
mix phoenix.new -v

Initializing the project

mix phx.new busi_api --no-brunch --no-html

We just created the app without HTML rendering and asset building as we don’t need to render static content.

Now open config/dev.exs and you’ll see that the app runs on port 4000. The default database adapter when creating a Phoenix app, is Postgres. Update your PostgreSQL credentials:

config :busi_api, BusiApi.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "busi_api_dev",
hostname: "localhost",
pool_size: 10

Back to your terminal, run these commands to create the app database and start the server:

cd busi_api
mix ecto.create
mix phx.server

If you see an error about plug_cowboy, add the package to mix.exs:

defp deps do 
[
...
{:cowboy, "~> 1.0"},
{:plug_cowboy, “~> 1.0”}
]
end

Run mix deps.get to install the package and then start the server again using mix phx.server and visit http://localhost:4000. You’ll see an error:

That’s because we haven’t defined any route yet! Let’s define a simple index route for now. Update router.ex like this:

We added pipeline :browser to allow handling HTML requests. You can read more about pipelines here. Now let’s create our lib/busi_api_web/controller/default_controller.ex:

Now reload the page and you won’t see an error, just a simple text.

Creating our first JSON API

mix help | grep phx

Suppose we want to have a list of active businesses. To create our JSON APIs with our model, we need to use mix phx.gen.json. Here we go:

mix phx.gen.json Directory Business businesses name:string description:text tag:string

You may ask “What is Directory?”. Phoenix 1.3 had some changes compared with older versions. One such change is that it allows us to separate domain logic into different modules called context. So, we decided to have all the logic about our business model (and probably other resources) in the Directory context which is a separate folder in the project.

The mix command just created a set of files including a controller and a migration file and some test files. Then Phoenix asks us to add this resource to our lib/busi_api_web/router.ex and update the database with mix ecto.migrate:

scope "/api", BusiApiWeb do
pipe_through :api
resources "/businesses", BusinessController, except: [:new, :edit]
end

Let’s run mix phx.routes to see our routes:

Before we test our APIs, let’s add some initial seed data. Open priv/repo/seeds.exs and add these lines:

alias BusiApi.Repo
alias BusiApi.Directory.Business
Repo.insert! %Business{name: "Company 1", description: "Short description ...", tag: "IT, Software"}Repo.insert! %Business{name: "Company 2", description: "Short description ...", tag: "Marketing"}Repo.insert! %Business{name: "Company 3", description: "Short description ...", tag: "Accounting"}

Now run mix run priv/repo/seeds.exs. We just used alias to create 2 aliases for Repo and Business modules to use them to create records. Restart your server and open http://localhost:4000/api/businesses to see the JSON records.

If you want to create a new record using the POST API, just run this curl command in your terminal (or use Postman):

curl -X POST http://localhost:4000/api/businesses -H “Content-Type: application/json” -d ‘{“business”: {“name”: “Yet another company”, “description”: “Another short description!”, “tag”: “IT”}}’

Note that we are passing the data in the “business” field as the API expects parameters to be sent in that field (Check business_controller.ex).

It’s time to investigate the code! Throughout this story, you’ll see an awesome operator called pipe (|>) which is really useful in case of multiple function calls. You may also want to read a bit about pattern matching before we continue.

Let’s start with BusinessController. We just learned about alias, so we skip that one. We have 5 actions (APIs) including index, show, create, update and delete. These actions call some functions from the Directory module to fetch/manipulate data objects. For instance, list_businesses() in the module returns all the records from the Business table:

def list_businesses do
Repo.all(Business)
end

Spend some time checking those modules to have a better understanding of what’s going on in the data layer of an Phoenix app.

Another interesting feature is action_fallback which needs some explanation. Basically, an action fallback is used to make the code simpler so that we can focus on the success status of those Repo functions. If something bad (like data validation or constraints violation) happens, the action_fallback statement calls the relevant function from FallbackController, otherwise the resource will be created or updated. The with statement only cares about {:ok, RESOURCE} tuple which is how most module functions work:

For example, create_business function may return a tuple like this after a success status:

{:ok,
%BusiApi.Directory.Business{
__meta__: #Ecto.Schema.Metadata<:loaded, "businesses">,
description: "DESCRIPTION",
id: 5,
inserted_at: ~N[2018-11-11 07:00:53.229068],
name: "NAME",
tag: "TAG",
updated_at: ~N[2018-11-11 07:00:53.231330]
}}

or an error status:

{:error,
#Ecto.Changeset<
action: :insert,
changes: %{},
errors: [
name: {"can't be blank", [validation: :required]},
description: {"can't be blank", [validation: :required]},
tag: {"can't be blank", [validation: :required]}
],
data: #BusiApi.Directory.Business<>,
valid?: false
>}

This is what we call Changeset which we’ll discuss more. You can test this by running iex -S mix in your terminal and:

alias BusiApi.DirectoryDirectory.create_business(%{name: "NAME", description: "DESCRIPTION", tag: "TAG"})

Now let’s check our business model:

Apart from defining the fields and their data types, there is a changeset function and its job is to cast the input parameters to the model fields and validate them. So, for example, we can run the previous curl command but this time without passing the “tag” field. You’ll see this error:

{“errors”:{“tag”:[“can’t be blank”]}}

The last thing we want to review is the JSON view which provides the output data via the APIs. Open views/business_view.ex:

If you want to change the output or add other fields (like timestamp fields: inserted_at, updated_at), this is where you should update the code. This module also has a nice way of rendering many data records with render_many. We can update the render function to return inserted_at field:

We used NaiveDateTime module to convert a datetime value to string. Refresh http://localhost:4000/api/businesses to see the result.

Unit test

MIX_ENV=test mix ecto.create
MIX_ENV=test mix ecto.migrate
mix test

You’ll see that there are 2 errors because we added another field to our business JSON output:

Let’s fix them. Open test/busi_api_web/controllers/business_controller_test.exs and update this assert statement which is used in 2 test cases, describe “create business” and describe “update business”:

assert json_response(conn, 200)["data"] == %{
"id" => id,
"description" => "some description",
"name" => "some name",
"tag" => "some tag",
"date" => NaiveDateTime.to_string(Directory.get_business!(id).inserted_at)
}

Now run mix test again and you’ll see all the tests are passed successfully.

Authentication with Guardian and Comeonin

Now run mix deps.get to install those packages. We need to create another JSON resource to manage our users (although we won’t use all the actions, it’s easier to use generators):

mix phx.gen.json Accounts User users email:unique encrypted_password:string

Update the router to reflect the user APIs (signup and signin):

scope "/api", BusiApiWeb do
pipe_through :api
resources "/businesses", BusinessController, except: [:new, :edit]
post "/users/signup", UserController, :create
post "/users/signin", UserController, :signin
end

Then run mix ecto:migrate to create users table.

Open lib/busi_api/accounts/user.ex to make some changes:

So we added a new field to the user schema — password — which is a virtual field. As you may have guessed, this new field won’t be persisted in the database but we use it for the validation process. encrypted_password will be saved in the DB. Also, we updated the changeset function for validation and format checking. To encrypt password, we created a private function — put_hashed_password — to hash the password using Comeonin/Bycrypt. This function gets the changeset (not-persisted user object) and updates encrypted_password if the changeset is valid.

Let’s create a user to make sure the above code works! In your terminal, run iex -S mix:

alias BusiApi.Repo
alias BusiApi.Accounts.User
Repo.insert!(User.changeset(%User{}, %{email: "user1@business.com", password: "password1"}))

You can also test the API using curl:

curl -X POST http://localhost:4000/api/users/signup -H "Content-Type: application/json" -d '{"user": {"email": "user1@business.com", "password": "password"}}'

Now let’s import Guardian properly! Open config/config.exs and add this to the end of the file:

config :busi_api, BusiApiWeb.Auth.Guardian,
issuer: "busi_api",
secret_key: "SECRET"

Replace SECRET with the output of mix guardian.gen.secret.

Then, we need to add an auth module to use Guardian (JWT is the default token type). Create lib/busi_api_web/auth/guardian.ex:

This module helps us create tokens, decode them, refresh tokens and revoke them. Now open user_controller.ex and add an alias to this module:

alias BusiApi.Accounts
alias BusiApi.Accounts.User
alias BusiApiWeb.Auth.Guardian

And update the create action and remove the other actions as we don’t need them here (index, show, update, delete):

We added a Guardian function to create a JWT token after a user is created. We also render “user.json” to send the JWT token as part of the response. But we need to update the view. Open lib/busi_api_web/views/user_view.ex and update it:

Now call the API:

curl -X POST http://localhost:4000/api/users/signup -H "Content-Type: application/json" -d '{"user": {"email": "user2@business.com", "password": "password"}}'

which responds with:

{"token":"eyJhbGciOiJIUzUxMi...", "email":"user2@business.com"}

Sign-in

Now we need a function to authenticate a user. We put it in lib/busi_api_web/auth/guardian.ex:

As you noticed, we added 3 functions in order to get a user by email, check if the password matches with encrypted password using checkpw function of Comeonin, and finally create the token. Look how we used pattern matching to get the token from Guardian encode_and_sign function:

{:ok, token, _claims} = encode_and_sign(user)

As we added another error status — unauthorized — we need to update fallback_controller.ex to act upon receiving this status by adding another call function:

def call(conn, {:error, :unauthorized}) do
conn
|> put_status(:unauthorized)
|> render(BusiApiWeb.ErrorView, :"401")
end

Create an action in user_controller.ex:

Now test the API:

curl -X POST http://localhost:4000/api/users/signin -H "Content-Type: application/json" -d '{"email": "user1@business.com", "password": "password"}'

Route authentication

And a module for error handling in lib/busi_api_web/auth/error_handler.ex:

Now we should add a new pipeline to our router.ex:

pipeline :auth do
plug BusiApiWeb.Auth.Pipeline
end

And update the api scope:

scope "/api", BusiApiWeb do
pipe_through :api
post "/users/signup", UserController, :create
post "/users/signin", UserController, :signin
end
scope "/api", BusiApiWeb do
pipe_through [:api, :auth]
resources "/businesses", BusinessController, except: [:new, :edit]
end

So we added the auth pipeline to APIs which we need to restrict the access. Now if you call /api/businesses, you’ll get an unauthenticated error:

curl -X GET http://localhost:4000/api/businesses

You need to send your JWT token — which you get after you sign in — to get the result:

curl -X GET http://localhost:4000/api/businesses -H "Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..."

Note that we are using HTTP Bearer authorization.

Congrats! The app is ready.

You can check other Guardian functions in case you need to, for example, access the current user or revoking a token for the purpose of logout:

current_user = Guardian.Plug.current_resource(conn){:ok, claims} = BusiApiWeb.Auth.Guardian.revoke(token)

Recap

I hope you enjoyed this article. You can download the source code from my Github: pamit/elixir-phoenix-json-api.

Happy coding!