We'll use mix to create an umbrella project. Umbrella project allows us to create a top level project and split our functionalities into different apps or child projects, so that we can specify dependencies, supervision trees and write code in different apps but compile/run the project at the top level like a normal mix project.

In this URL shortener project, it's obvious to us that it can be divided into to parts: storage and web server. Storage will handle how data is stored and provide interfaces where we can do CRUD operations and web server will handle API requests as well as redirecting users.

First, create the top level project

mix new exshort --module ExShort --umbrella

in the command above, exshort is the folder name mix is going to create, and --module specifies the module name, if we omit it here, the module name is going to be Exshort. --umbrella is the flag given to mix to create a skeleton of umbrella project.

Running this command should give us a folder named exshort, and it's content should look like this:

I really like the experience of Cake and think it can be a great platform for writing tutorials and share code snippets that are not too long. So I decided to try to write my first tutorial in Cake. Any feedback will be appreciated.

Some feedback to the platform. I know Cake is not intended to share code snippets, but the ability of coding highlighting and inline code style would be great to have.

Mnesia is a distributed telecommunications DBMS, and it is a built-in module in Erlang, so we don't need to specify any dependencies in our ExShortStorage app. The reason we are using Mnesia here, besides the fact it's built-in is:1. I can write more stuff about Elixir rather than other services such as redis and sqlite. 2. Mnesia itself is a neat DBMS. it is a distributed system, which means we can deploy our service across multiple machines without the need to maintain a centralized database server. Also, it handles concurrent transactions correctly, which can be tricky to do if we are to use redis or sqlite.

Before writing code to files, let's get ourselves familiar with Mnesia in Elixir's interactive shell (iex):

type "iex" in your terminal to enter the interactive shell, and we will use 5 commands to create a schema, create a table, and save a Link record in it.

On line 1, we created a schema. You can think of it as a database in MySql or a database file in Sqlite. we used the atom :mnesia to call the Erlang module from elixir, and executed the function create_schema with parameter [node()]. Atom is a basic data type in Elixir, similar to symbol in other languages such as javascript. You can think of it as a constant whose value is it's own name (the name of an atom is pretty much all the information you can get from it). Atoms usually starts with ':', but names starting with upper case letters are also atoms, eg, is_atom(A) would return true. Elixir uses atoms to reference modules, that's why we can use :mnesia to call mnesia module. Built in Elixir modules, eg, Emun and List, are all named by atoms. But the convention is that modules in Elixir are CamelCased, whereas native Erlang modules are named by lower case atoms. Node() just returns an atom representing the name of the local node. As a language built with distributed systems in mind, Node is an important concept in Elixir, but we are not going into too much details here. Running this command, Mnesia would create a folder in the current directory where it persists stored data. It returns an atom :ok, meaning the operations is successful.

On line 2, we start the Mnesia application. In a single node configuration, the process is rather simple but with a multi node configuration it actually talks to all the nodes, syncs some data and make sure they can work together. It returns an atom :ok, meaning the operations is successful.

On line 3, we create a table called Link (which is an atom), and specifies the two attributes it has: :slug and :url, in order.

On line 4, we define a inline function with named writer, in the body of which, we wrote database operations. In this case, we are writing a Link record with attributes :slug and :url specified in order. In the example above, we are telling it to create a Link, with url set to "https://google.ca", and slug set to "goog". Inline functions are kinda like lambdas in python and arrow functions in javascript. The syntax to create a inline function is

fn parameters -> ...do_something end

for example, we can do

foo = fn a, b -> a + b end
foo.(1,2)
# returns 3

Note that in order to call inline functions directly, we need to append a dot at the end of it's name, which is different from a function that lives in a module, eg: :mnesia.create_table.

On line 5, we ran the operations specified on line 4 inside a transaction, telling it to apply operations we specified on line 4 and commit changes to database. It returned {:atomic, :ok} signifying that the transaction is successfully applied to database. The benefit of a transaction is that if one of the operations inside a transaction failed, the state of database will rollback, instead of ending up in a "dirty" state.

To confirm that data is indeed stored in the database, we can try to read it back: