Cake
  • Log In
  • Sign Up
    • Elixir is a dynamic, functional language designed for building scalable and maintainable applications.

      Here we'll walk through the process of building a URL shortener API in Elixir, using Plug and Mnesia.
      Hopefully this can serve as a introduction to Elixir language.

    • Hey @ericls! Looks like you may have published this post before it was finished. Or maybe you're still working on additional posts? Either way, I'm interested to read the rest of the tutorial. πŸ™‚

    • Creating our project

      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:

      exshort
          β”œβ”€β”€ apps
          β”œβ”€β”€ config
          β”‚Β Β  └── config.exs
          β”œβ”€β”€ mix.exs
          └── README.md

      If you used mix to create elixir projects before, you'll notice when given --umbrella, it generates less file for us.

      Because the web server is dependent on the storage layer, so let's start by creating the storage app:

      cd exshort/apps
      mix new ex_short_storage --module ExShortStorage --sup

      This command would create an app (aka sub project) inside the apps folder and the resulting file system layout would look like this:

      .
      β”œβ”€β”€ apps
      β”‚Β Β  └── ex_short_storage
      β”‚Β Β      β”œβ”€β”€ config
      β”‚Β Β      β”‚Β Β  └── config.exs
      β”‚Β Β      β”œβ”€β”€ lib
      β”‚Β Β      β”‚Β Β  β”œβ”€β”€ ex_short_storage
      β”‚Β Β      β”‚Β Β  β”‚Β Β  └── application.ex
      β”‚Β Β      β”‚Β Β  └── ex_short_storage.ex
      β”‚Β Β      β”œβ”€β”€ mix.exs
      β”‚Β Β      β”œβ”€β”€ README.md
      β”‚Β Β      └── test
      β”‚Β Β          β”œβ”€β”€ ex_short_storage_test.exs
      β”‚Β Β          └── test_helper.exs
      β”œβ”€β”€ config
      β”‚Β Β  └── config.exs
      β”œβ”€β”€ mix.exs
      └── README.md

      If you take a look at apps/ex_short_storage/mix.exs, you'll notice that mix knows this app is inside an umbrella project and sets build_path, config_path, etc. accordingly.

      In next section, we'll start implementing functionalities of ExShortStorage using Mnesia.

    • Hi Ryan,

      Thanks for showing interests in this topic.

      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.

    • Creating Storage Handling Module using Mnesia(Part I)

      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.

      iex(1)> :mnesia.create_schema([node()])
      :ok
      iex(2)> :mnesia.start()
      :ok
      iex(3)> :mnesia.create_table(Link, attributes: [:slug, :url])
      {:atomic, :ok}  
      iex(4)> writer = fn -> :mnesia.write({Link, "goog", "https://google.ca"}) end
      #Function<20.127694169/0 in :erl_eval.expr/5> 
      iex(5)> :mnesia.transaction(writer)
      {:atomic, :ok}

      Let's walk through what happened here.

      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:

      iex(6)> reader = fn -> :mnesia.read({Link, "goog"}) end
      #Function<20.127694169/0 in :erl_eval.expr/5>
      iex(7)> :mnesia.transaction(reader)
      {:atomic, [{Link, "goog", "https://google.ca"}]}

      As we can see, we are able to find the Link with slug "goog".

    You've been invited!