Purpose

In this post we learn how to implement a REST Api with Elixir. For this, we will make use of Maru is a micro framework for Elixir insspired by grape.

In the previous post Getting started with Elixir - Ecto we just defined some basic concepts about Elixir so from now on we will skip some basic details.

Setting up project

Since we just created a project in a previous post we will add to this project as a dependency

mix.exs

    ...
def application do
[applications: [:logger, :maru, :elixir_ecto_training]]
end
...
defp deps do
[
{:maru, "~> 0.10"} ,
{:elixir_ecto_training, git: "https://github.com/wesovilabs/elixir_ecto_training.git", tag: "0.1.0"}
]
end

Maru configuration

We will make the below configurations:

  • We can configure the port where the application will run on.
  • We add versioning capability to our Rest Api.

/config/config.ex

use Mix.Config

config :maru, ElixirMaruTraining.API,

versioning: [
using: :path
],
http: [port: 5001]

Rest Api version

Maru provides us with three different ways to implement Rest Api

  • By path
  • By param
  • By Accept version header

The Maru documentation is really clear and they provide with so I invite you to have a look at it.

The Api

First of all we define an API module. This module is the one that we stated in the config.ex, and we define some global properties for our endpoints

    before do
plug Plug.Logger
plug Plug.Parsers,
pass: ["*/*"],
json_decoder: Poison,
parsers: [:json]
end

Maru allow us to intercept exceptions and allow us to transform the response message by making use of rescue_from, see below:

    rescue_from Unauthorized, as: e do
IO.inspect e
conn
|> put_status(401)
|> json(%{message: "This place is not for you, you are unauthorized"})
end

rescue_from Maru.Exceptions.NotFound, as: e do
Logger.debug "404: URL Not Found at path /#{e.path_info}"
conn
|> put_status(404)
|> json(%{message: "Hey budy you have no idea where you want to go"})
end

rescue_from :all, as: e do
IO.inspect e
conn
|> put_status(500)
|> json(%{message: "Something went bad! And I do not know what..."})
end

Since we are making use of versions we will implement a couple of routers (v1 and v2) and this routers are referenced from Api

    mount ElixirMaruTraining.TrackRouterV1
mount ElixirMaruTraining.TrackRouterV2

Then the ElixirMaruTraining.Api module look like this

  defmodule ElixirMaruTraining.Api do
use Maru.Router
require Logger

before do
plug Plug.Logger
plug Plug.Parsers,
pass: ["*/*"],
json_decoder: Poison,
parsers: [:json]
end

mount ElixirMaruTraining.TrackRouterV1
mount ElixirMaruTraining.TrackRouterV2

rescue_from Unauthorized, as: e do
IO.inspect e
conn
|> put_status(401)
|> json(%{message: "This place is not for you, you are unauthorized"})
end

rescue_from Maru.Exceptions.NotFound, as: e do
Logger.debug "404: URL Not Found at path /#{e.path_info}"
conn
|> put_status(404)
|> json(%{message: "Hey budy you have no idea where you want to go"})
end

rescue_from :all, as: e do
IO.inspect e
conn
|> put_status(500)
|> json(%{message: "Something went bad! And I do not know what..."})
end
end

Resources

For this example we define a new structure in our code. This will be the response structure that our services will return.

/lib/elixir_maru_training/resources.ex

  defmodule ElixirMaruTraining.TrackResource do

defstruct [:title, :singer]

end

Routers

In other programming languages are known as controllers or even services. Basically a router is the place where we define the endpoints for our applications

In the Api we defined pointed to the Routers:

  • TrackRouterV1: This version will available a couple of services which will return mocked data.

    • GET /v1/tracks - Return the list of mocked tracks
    • POST /v1/tracks - Add a new track and return a 204 http status code

    The router is implemented in /lib/elixir_maru_training/router/track_router_v1.ex

    defmodule ElixirMaruTraining.TrackRouterV1 do
use Maru.Router
require Logger
alias ElixirEctoTraining.Track, warn: true
alias ElixirMaruTraining.TrackResource
def allTracks do
[
%TrackResource{title: "Todos los dias sale el sol", singer: "Bongo Botrako"},
%TrackResource{title: "Mi jefe", singer: "Mojinos Escozios"}
]
end

namespace :tracks do

version "v1" do

@desc "Return the list of mocked tracks"
get do
tracks = allTracks()
Logger.debug "Tracks #{inspect tracks}"
conn
|> put_status(200)
|> json(tracks)
end


@desc "Add a new track and return a 204 http status code"
params do
requires :title, type: String
requires :singer, type: String
optional :score, type: Atom, values: [:bad, :normal, :good, :awesome], default: :normal
end
post do
track = %Track{title: params[:title], singer: params[:singer]}
conn
|> put_status(204)
|> json(%{})
end
end
end

end

In the above example we make use of the Maru Pipelines

   conn
|> put_status(200)
|> json(tracks)

as all of us can imagine what we are doing is return a 200 http status code and the body will be a list of track resources.

For the POST endpoint we need to know the request body, and we do it by defining a parameter as see in the example

  params do
requires :title, type: String
requires :singer, type: String
optional :score, type: Atom, values: [:bad, :normal, :good, :awesome], default: :normal
end

this is awesome since we can even define enum values, set the attribute nature, set if the pattribute is optional or required…

We can check the service are working by running the below commands:

curl -XGET http://127.0.0.1:5001/v1/tracks  -H "accept: application/json" -i

curl -XPOST http://127.0.0.1:5001/v1/tracks -i -H "accept: application/json" -H "Content-Type:application/json" --data '{"title":"La Macarena", "singer":"Los del rio"}'
  • TrackRouterV2: This version will again implement the get tracks and create new track services but this time we will make use of elixir_ecto_training project. To sum up this time we will work with real data.

    The code is below.

  
defmodule ElixirMaruTraining.TrackRouterV2 do
use Maru.Router
import Ecto.Query
require Logger
alias ElixirEctoTraining.Track, warn: true
alias ElixirEctoTraining.Repo, warn: true


def allTracks do
query = from track in ElixirEctoTraining.Track,
select: (
%{ trackId: track.id, title: track.title, singer: track.singer}
)
query |> ElixirEctoTraining.Repo.all
end

def inserTrack(track) do
ElixirEctoTraining.Repo.insert(track)
end

namespace :tracks do

version "v2" do

@desc "Return the list of tracks in the database"
get do
tracks_as_json = allTracks()
|> List.wrap
|> Poison.encode!
Logger.debug "Tracks #{inspect tracks_as_json}"
conn
|> put_status(200)
|> json(tracks_as_json)
end



@desc "Add a new track and return a 201 http status code"
params do
requires :title, type: String
requires :singer, type: String
optional :score, type: Atom, values: [:bad, :normal, :good, :awesome], default: :normal
end
post do
track = %Track{title: params[:title], singer: params[:singer]}
inserTrack(track)
conn
|> put_status(204)
|> json(%{})
end
end

end

end

As we can observe the difference between v1 and v2 is that this time we are really taking the data from de database instead of using mocked data.

We can check the service are working by running the below commands:

  curl -XGET http://127.0.0.1:5001/v2/tracks  -H "accept: application/json" -i
  
  curl -XPOST http://127.0.0.1:5001/v2/tracks -i -H "accept: application/json" -H "Content-Type:application/json" --data '{"title":"La Macarena", "singer":"Los del rio"}'

Maru provides us with a task that allowas identify the routes in our application

mix maru.routes

The code

As always the code can be found on wesovilabs repository