5 tips & code recipes for your next Elixir/Phoenix project
The clearer the source code of your Elixir/Phoenix application, the better. If you need tips on how to make it simpler, read this tutorial.
A practical Elixir/Phoenix programming Tutorial: 5 tips for a simpler code
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 Elixir/Phoenix programming 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. A 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 an 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!