I believe that in Elixir web development and software development as such it is best to “keep things simple, stupid”. So without further ado I will share with you 5 tips and tricks we use to simplify our codebase at Prograils.

Custom Ecto Schema

Sometimes you want to use a different Ecto configuration - for example to use UUIDs instead of IDs. This approach makes it easy to configure Ecto globally and prevent mistakes.

defmodule App.Schema do
 defmacro __using__(_opts) do
   quote do
     use Ecto.Schema
     import Ecto
     import Ecto.Changeset
     import Ecto.Query

     @primary_key {:id, :binary_id, autogenerate: true}
     @foreign_key_type :binary_id

     @type t :: %__MODULE__{}
   end
 end
end

With this approach you can use many “top level” schema definitions as well if you need a different implementation for a certain context/umbrella app.

Custom Ecto Validator

There are validations you need to do often - a good example is string validation. :string field that we use in migration has a maximum size of 255 characters. Common approach would be to use validate_length(changeset, key, max: @max_string_length) but to keep your code cleaner u can organize this differently:

  • create App.Validator module which contains custom validation functions (like string length, phone numbers),
  • import App.Validator in your project schema,
  • now you can use validate_max_string_length/1 in your changesets ,

With this approach, you can write 2 implementations of validation functions - one if you pass a single field, and one for accepting an array of fields as argument.

Reversed 'with' statement

The with statement is commonly used in Elixir programming - however if it should be so popular is a debatable thing. There is a “reverse with” approach you can use when calling commonly failing functions and is definitely a perfect usage for “with clause”.

with {:error, _} <- App.Backup.fetch_from_memory(backup_id),
    {:error, _} <- App.Backup.fetch_from_disk_storage(backup_id)
    {:error, _} <- App.Backup.fetch_from_s3(backup_id) do
 {:error, :fatal_error}
else
 {:ok, backup} -> backup
end

As you can see, in this approach we try to download a backup from many sources. We try to get it from the memory first, then from the disk and if that fails, we use the s3 backup. The With statement here makes code easy to read and logical as well.

Adapter and Test adapter

Mocks can sometimes be messy. In Elixir you can easily test functions that require remote services using Adapters and behaviours. Here is an example on how to send files to a remote server.

We want to have an option to send a file to remote storage. Let's define the behaviour first:

defmodule App.Adapter do
 @callback send_file(String.t(), String.t()) :: :ok | {:error, Atom.t()}
end

Now we wantto create an FTP upload module.

defmodule App.FTPAdapter do
 @behaviour App.Adapter

 def send_file(local_path, remote_path) do
   {:ok, pid} = setup_connection()
   result = send(pid, local_path, remote_path)
   close_connection(pid)

   result
 end
 ...
end

...and an Adapter for the test environment.

defmodule App.TestAdapter do
 @behaviour App.Adapter

 def send_file(_local_path, _remote_path),
   do: :ok
end

Finally, we can create an interface module.

defmodule App.RemoteUpload do
 def send_file(local_path, remote_path),
   do: adapter().send_file(local_path, remote_path)

 defp adapter(),
   do: Application.get_env(:app, App.RemoteUpload)[:adapter]
end

With this approach we can: - use TestAdapter when running ExUnit, - use FTPAdapter in production environment, - configure the App.RemoteUpload with new adapter if you need to add an extra behaviour.

Function Compositions in Elixir/Phoenix

Let's say we need to create a custom id for a model we are using. Function compositions can let you do it nicely and easy to change in the future.

defmodule App.Product do
 defstruct [:inserted_at, :updated_at, :name, :type]
end

defmodule App.Products do
 alias App.Product
 def generate_publisher_id(product) do
   ""
   |> add_year(product)
   |> add_delimiter()
   |> add_name(product)
   |> add_delimiter()
   |> add_type(product)

 end

 defp add_delimiter(string) do
   string <> "-"
 end

 defp add_year(string, %Product{inserted_at: %DateTime{year: year}}) do
   string <> "#{year}"
 end

 defp add_name(string, %Product{name: name}) do
   string <> name
 end

 defp add_type(string, %Product{type: type}) do
   string <> type
 end
end

x = %App.Product{
 inserted_at: DateTime.utc_now(),
 updated_at: DateTime.utc_now(),
 name: "Product",
 type: "X"
}
App.Products.generate_publisher_id(x)

This example is quite straightforward, but the more complex the composition functions are, the more you will benefit from that approach.

Enjoyed this article? Check out the one on Elixir libraries.

Do you know more tips to simplify code in an Elixir/Phoenix project? Let me know in the comments section!