Elixir |> Building a Simple API

In this blog post I will be focusing on building a simple API to keep a todo list. I will be using maru an Elixir RESTful Framework. All responses will be json and the “persistence” store I chose is an Agent. This means state is maintained only while the agent process is alive. I could have used Ecto to interface with a database but that would make this blog a little bit longer than desired.

Getting started

Create a new mix application

mix new todo --sup

Add maru as a dependency

defmodule Todo.Mixfile do use Mix.Project def project do  
  [app: :todo, version: "0.0.1", elixir: "~> 1.0", build_embedded: Mix.env == :prod, 
  start_permanent: Mix.env == :prod, deps: deps]
end

def application do  
  [applications: [:logger, :maru], mod: {Todo, []}] 
end  
  defp deps do [ {:maru, "~> 0.2.8"} ] 
end  
end  

Now add maru to the applications list to make sure it gets started when we start the todo application.

defmodule Todo.Mixfile do use Mix.Project  
  def project do [app: :todo, version: "0.0.1", 
  elixir: "~> 1.0", build_embedded: Mix.env == :prod, 
  start_permanent: Mix.env == :prod, deps: deps] 
end 

def application do  
  [applications: [:logger, :maru], mod: {Todo, []}] 
end 

  defp deps do [ {:maru, "~> 0.2.8"} ] 
end  
end  

Get the dependencies and compile them using mix

mix do deps.get, compile

Configure the port and entry point (module) for the API

Open config/config.exs and remove the comment on the last line so we may use different configurations for development, test and production.

use Mix.Config  
import_config "#{Mix.env}.exs"  

If you try to run the application now you will get an error message as mix is not able to load the development environment configuration file. To solve this we will create configuration files for each environment: prod, dev and test.

Run the following on the command line:

echo "use Mix.Config\n\nconfig :maru, Todo.Api,\n http: [port: 8800]" > dev.exs

echo "use Mix.Config\n\nconfig :maru, Todo.Api,\n http: [port: 8801]" > test.exs

echo "use Mix.Config\n\nconfig :maru, Todo.Api,\n http: [port: 8802]" > prod.exs

Different ports are defined based on the environment. One possible use case is running tests without stoping an application that may be running in development mode(or in production).

Now before the application can be successfully started the maru entry point(Todo.Api) defined in the configuration files needs to be defined.

Create the lib/todo/api.ex file with the following content:

defmodule Todo.Api do  
   use Maru.Router 
end  

To make sure everything is properly set up and runs as expected, start the application in an IEx session

iex -S mix

and get some info on the applications currently running:

:application.which_applications

If todo and maru are listed in the running applications it’s time to move on!

Implementing the Router

The API routes will be defined in a single module

defmodule Todo.Api do  
  use Maru.Router alias Todo.AgentWorker, as: Store 

namespace :tasks do  
 desc "get all tasks" 
  get do Store.get |> json 
end 

desc "creates a task"  
  params do 
  requires :description, type: String 
  requires :completed, type: Boolean, default: false 
end  
post do Store.insert(params) |> json  
end  
  route_param :id do 
   desc "get a task by id" 
   get do 
    Store.get(params[:id]) |> json 
end 

desc "updates a task"  
 params do 
  optional :completed, type: Boolean 
  optional :description, type: String 
  at_least_one_of [:completed, :description] 
end  
put do  
  Store.update(params) |> json 
end 

 desc "deletes a task" 
 delete do Store.delete(params[:id]) |> json 
 end 
 end 
end 

 def error(conn, _e) do 
 %{error: _e} |> json 
end  
end  

The tasks namespace is defined in line 5 and under it, the get and post endpoints. At line 20 the route_param block is defined. The endpoints that depend on an id value are declared inside this block.
Maru makes it really easy to define required params and their types and one great feature is the ability of marking all params as optional and still require that at least one of them is passed (lines 27-31).

Error handling occurs in lines 43-45, and the error messages will also be returned as json.

Using an Agent as the data store

We will use an Agent to hold the state, the tasks will be stored inside this abstraction. It would be possible to use ETS tables or a database but as I already mentioned I wanted to keep things as simple as possible.

I have implemented some basic operations that allow to connect the api and work on the task resources.

  defmodule Todo.AgentWorker do 
   @name __MODULE__ 
  def start_link do Agent.start_link(fn -> %{} 
 end, name: @name) 
end 

def insert(params) do  
 id = get_id_value 
 Agent.update(@name, &Map.put_new(&1, id, Map.merge(%{id: id}, params))) 
end 

def update(params) do  
  Agent.update(@name, &Map.put(&1, params[:id], params)) 
 end 
 def get do Agent.get(@name, &Map.values(&1)) 
end  
 def get(id) do 
 Agent.get(@name, &Map.get(&1, id)) 
end 

def delete(id) do  
 Agent.update(@name, &Map.delete(&1, id)) 
end 

defp get_id_value do  
 :crypto.rand_bytes(16) |> Base.encode16 |> String.downcase 
 end 
end  

This AgentWorker is started by the application supervision tree

defmodule Todo  
  do use Application def start(_type, _args) 
   do import Supervisor.Spec, warn: false children = [ worker(Todo.AgentWorker, []) ] 
  opts = [strategy: :one_for_one, name: Todo.Supervisor] 
  Supervisor.start_link(children, opts) 
 end 
end  

To test the application start it using the following command:

iex -S mix

Now on another terminal use curl to interact with the API:

# Create a new task 
curl -X POST --data "completed=false&description=demo" http://localhost:8800/tasks/ 

# Retrieve all tasks 
curl http://localhost:8800/tasks 

# Retrieve a task with id 6fc4258a684fc3d8c70cb1d66a1d7ee4 
curl http://localhost:8800/tasks/6fc4258a684fc3d8c70cb1d66a1d7ee4 

# Update a task curl -X PUT --data "completed=true&description=demo" http://localhost:8800/tasks/6fc4258a684fc3d8c70cb1d66a1d7ee4 

# Delete a task 
curl -X DELETE http://localhost:8800/tasks/6fc4258a684fc3d8c70cb1d66a1d7ee4  

All code can be found here at Onfido’s github.

As a side note: if you wish to replace the persistence store its just a matter of implementing the get/0, get/1, update/1, insert/1 and delete/1 functions found on the Todo.AgentWorker and then on Todo.Api replace line 3 with something like

alias YOURPERSISTENCEMODULE_NAME, as: Store

Next blog post will be about BDD in Elixir.

Author image
Author, blogger, father and Ruby/Rails/Elixir guru, there's very little Paulo doesn't do. And best of all, he does it with a smile and (not so funny) jokes to boot!
top