Building small elixir services using Ecto (without Phoenix)

Andrew Forward
5 min readApr 19, 2017

--

In this article you will learn how to generate your small elixir project to leverage a Postgres database using Ecto without the use of Phoenix. Skip the next section if you are raring to go!

Let’s get a few things out of the way first. Phoenix is awesome, this is not a gripe against it but rather a how-to for moving your code into smaller projects that your phoenix app can then leverage. I prefer to build phoenix as complete standalone (versus an umbrella application). That said, this process will still work fine in an umbrella app.

Next, despite Ecto (and Elixir) making great strides in removing a lot of boilerplate; there are still quite a few moving parts to juggle in your head when starting up a new application. Much of which is difficult to remember, so a little bootstrap helps get the ball rolling.

Next, I am purposely not calling this microservices as the bootstrapped project does not provide any ability to expose the service as RESTful. There is nothing stopping you for starting here and then incorporating REST using Phoenix, Maru, or from scratch with Plug. The point of this article is to help you bootstrap the persistence portion of your project with a connection to the database via Ecto.

Finally, Postgres isn’t the only option for your persistence layer. That said, this article is focused on it, so if you are looking for help boostrapping ETS, MongoDB, Redis or some other storage mechanism, you probably won’t find much help here.

Let’s start with the end in mind. You want to create a small service for storing names for use in your software programs (it’s one of two hard problems). For this example, we will just be storing names with descriptions that’s it.

Like most Ecto projects, we will need to create a Repo, configure the databases, create migrations and setup testing. Dave Thomas created a great mechanism to create your own project template using mix gen (versus mix new)

Let’s follow the instructions from the readme to get started.

mix archive.install mix_templates
mix archive.install mix_generator
mix template.install hex gen_template_ecto_service

For our example, let’s name our project namely.

mix gen ecto_service namely
mix deps.get

That is going to generate a scaffolding for your project, and download all dependencies. Again, for more details on template generation, then check out Prag Dave’s article (and video) on the subject. I wanted to focus on where to go next with our service to make it your own.

If you haven’t already, go and create a remote repository using GitHub, or Bitbucket. What follows is specific to GitHub, but feel free to use which ever service you like (just change the remote URL accordingly).

git init
git add .
git commit -m "Initial commit from ecto_service"
git remote add origin git@github.com:aforward/namely.git
git push -u origin master

We now have a functional project, albeit it doesn’t do much. Let’s play around with the service that’s available out of the box.

Create your empty database, and start a repl

mix ecto.reset
iex -S mix

Now we can log actions against our service for when we create a new name (more on that later).

iex(0)> Action.add("name_added", "Name", "invoice")

Notice how we referenced the module by Action and not Namely.Action. That’s because the project template includes a .iex.exs file to alias a few namespaces.

That logs an action in our database against the Name invoice. Later we could register aliases for that name and then log the action.

%Action{} |>
Action.changeset(%{name: "alias_added", entity_type: "Name", entity_id: "invoice", data: %{alias: "bill"}}) |>
Repo.insert

Here we are calling the changeset directly so we can log additional information. Let’s peek in the database now to see those logged actions.

10:15 ~/project/namely (master)$ psql namely_dev
psql (9.6.2)
Type "help" for help.namely_dev=#

And the query the actions table.

namely_dev=# select * from actions;

Which returns two records (I left out the inserted_at and updated_at as they added extra (unnecessary) wrapping.

id |    name     | entity_type | entity_id |       data
----+-------------+-------------+-----------+-------------------
1 | name_added | Name | invoice |
2 | alias_added | Name | invoice | {"alias": "bill"}
(2 rows)

Next, let us make this service our own.

Let’s start with a names table.

mix ecto.gen.migration create_names

And the migration

defmodule Namely.Repo.Migrations.CreateNames do
use Ecto.Migration
def change do
create table(:names) do
add :name, :string
add :description, :string
timestamps()
end
create unique_index(:names, [:name])
end
end

Let’s apply the migrate (and update .iex.exs aliases)and play around in the REPL.

mix ecto.migrate
iex -S mix

Let’s create a few names

iex> Name.create("destroy", "Remove a record leaving no trace")
iex> Name.create("save", "Persisting a record somewhere.")
iex> Name.create("delete", "Remove a record")
iex> Name.all
13:48:01.365 [debug] QUERY OK source="names" db=3.1ms
SELECT n0."name" FROM "names" AS n0 ORDER BY n0."name" []["delete", "destroy", "save"]

Let’s now make use of the log actions to log when a name is added.

def create(name, description) do
%Name{}
|> Name.changeset(%{name: name, description: description})
|> Repo.insert
|> log_action("create_name", %{name: name})
end

We add a new function log_action that will push the information into the actions table against the name we are creating.

defp log_action({:ok, name}, action, data) do
log_action(
name,
action,
name.name,
data)
end

Using pattern matching, we can track the success and failure scenarios separately

defp log_action({:error, changeset} = answer, action, data) do
log_action(
answer,
"#{action}_failed",
changeset.changes[:name],
data) # TODO: seralize the changeset.errors
end

Note that I need to serialize the changeset to properly add it to the logged action. And the underlying saving of the information pushes into Action schema.

defp log_action(answer, action, entity_id, data) do
%Action{}
|> Action.changeset(
%{name: action,
entity_type: "Name",
entity_id: entity_id,
data: data})
|> Repo.insert!
answer
end

Happy Coding.

--

--

Andrew Forward

Back to PHP and #elixir; flirting with #Elm. TDD infected since jUnit 2.1, former cheesemaker, and working at #crossfit