We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
How do you efficiently set up development-only logic in Elixir? In one directory: _dev
.
This directory will contain all your development-only logic. Combined with config/dev.exs
configuration, this makes a clear and safe separation from your production logic. There will be no need for awkward Mix.env() == :dev
blocks around inline code (apart from two Phoenix files). The following example will be based on a Phoenix project, but you can use this pattern for any Elixir project.
Let us get started!
mix.exs
and .formatter.exs
First, we want to include _dev
files in our dev
and test
environments by updating elixirc_paths
in mix.exs
:
defmodule MyApp.MixProject do
use Mix.Project
# ...
# Specifies which paths to compile per environment.
# We include `_dev` directory in the test/dev env to
# enable the dev tools
defp elixirc_paths(:test), do: ["lib", "test/support", "_dev", "test/_dev/support"]
defp elixirc_paths(:dev), do: ["lib", "_dev"]
defp elixirc_paths(_), do: ["lib"]
# ...
end
We also want mix format
to pick up on it, so update the .formatter.exs
:
[
import_deps: [:phoenix],
plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{_dev,config,lib,test}/**/*.{heex,ex,exs}"]
]
That is it. Now you can put all your dev-only files in the _dev
directory and it will only ever get included in the dev
and test
environments. We can test our dev-only logic and add dev-only test support files to test/_dev/support
.
Example: Seed tool
Let us set up a development helper to manually seed a variety of scenarios in our environment.
First, we may want to add some helpers in _dev/my_app
, for example _dev/my_app/seed_helper.exs
:
defmodule MyApp.Dev.SeedHelper do
# ...
end
I’m namespacing the module names with Dev
. Though it’s just a dev-only tool we still need to test it, so we’ll create the test file in test/_dev/my_app/seed_helper_test.exs
:
defmodule MyApp.Dev.SeedHelperTest do
use MyApp.DataCase
# ...
end
For Phoenix, we’ll have to update our router to enable our dev-only routes. This along with navigation in the UI will be the only parts that can’t be completely separated from anything that touches production code.
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# ...
# Enable dev tools for dev and test env
if Application.compile_env(:my_app_web, :dev_tools) do
scope "/dev" do
pipe_through :browser
scope "/tools", MyAppWeb.Dev.Tools do
scope "/seeds" do
live "/", SeedLive, :index
live "/:id/run", SeedLive, :run
end
end
end
end
end
In this case, I set a config to enable the dev tools in config/config.exs
as:
config :my_app_web, :dev_tools, Mix.env() in [:dev, :test]
You could also just use Mix.env()
straight in the router, but I found the config pattern more comfortable.
We should add a link in our template as well:
<%= if Application.get_env(:my_app_web, :dev_tools) do %>
<.link navigate={"/dev/tools/seed"}>Seed</.link>
<% end %>
Now we’ll set up our dev tools, it could be in _dev/my_app_web/tools/live/seed_live.ex
:
defmodule MyAppWeb.Dev.Tools.SeedLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :seeds, list_seed_files())}
end
# ...
end
A test for the above should be in test/_dev/my_app_web/tools/seed_live_test.exs
.
defmodule MyAppWeb.Dev.Tools.SeedLiveTest do
use MyAppWeb.ConnCase
test "lists and run seeds", %{conn: conn} do
# ...
end
end
This has worked very well for my team, and I highly recommend this pattern.
Hi, I'm Dan Schultzer, I write in this blog, work a lot in Elixir, maintain several open source projects, and help companies streamline their development process