For some more concrete details, here are some patterns or rules of thumb I've noticed about when to use .ex
vs .exs
files:
Almost all code goes in an .ex
file in my application's 'library' directory tree, e.g. under lib
(or maybe web
if the application is an old Phoenix webapp). The code can be called (if it defines public functions or macros) elsewhere in the application or from iex
. This is the default.
Probably the most important consideration for deciding between .ex
and .exs
is where (and when) I want to call that code:
- In the application itself –
.ex
- From
iex
(e.g. iex -S mix
) – .ex
, if the code is also (or probably will be) used by the application; .exs
for 'ad-hoc' scripts, e.g. for a specific bug or other issue
- From the shell, via
mix
– .ex
for custom Mix tasks, or as data in mix.exs
(for simple task aliases)
- From the shell, i.e. as a 'regular script' –
.exs
and run via elixir some-script.exs
.
What may be surprising is that a good bit of code that starts as one of [2], [3], or [4], eventually ends up in [1] anyways.
And a really important thing to keep in mind is that it's EASY to move code from, e.g. an .exs
file to an .ex
file (or vice versa).
For [2], and for one application in particular, I setup some extra 'scripts mode' Mix environments for use with iex
. Basically, they run a basic 'dev' local instance of (parts of) the app, but connect to the production/staging DB (or other backend services). I have some helper functions/macros for these 'command environments' but mostly I just use application code to, e.g. query the production/staging database and fix/cleanup/inspect some data. If I do need to do something that's even a little 'involved' (complicated), I'll usually create an ad-hoc 'issue script'. It's much easier to write code in my editor than in iex
.
For ad-hoc scripts for [2], I have a dev/scripts
sub-directory-tree in one project and mostly just name ad-hoc scripts based on the corresponding ticket number (from that project's ticket system), ex. 123.exs
. Then, in iex
, I can easily run that script:
iex> c "dev/scripts/123.exs"
One nice 'workflow' is to just define a single (top-level) module in the script file:
defmodule Ticket123 do
...
def do_some_one_off_thing ...
end
Then, from iex
:
iex> c "dev/scripts/123.exs"
[Ticket123]
iex> Ticket123.do_some_one_off_thing ...
Something something
...
For [3], both task aliases in your project's mix.exs
and 'fully' custom Mix tasks are really nice.
Some example aliases from one project:
- Run 'all' migrations – this project has regular (
Ecto
) database migrations AND some migrations that we want to run 'manually' (i.e. using the 'scripts mode' environments mentioned above). We use manual migrations for things like long-running database queries/commands, ex. adding or modifying an index on a big table.
- Deploy to staging/production
test
– this shadows the builtin Mix task to perform some custom local-database setup
todo
– output search results for TODO comments in our (Elixir) code
Some example custom tasks from the same project:
Mix.Tasks.OneProject.BootstrapServer
– some custom { configuration management / automated infrastructure deployment } code
Mix.Tasks.OneProject.DbSetup
– pulls some configuration data from the Elixir app but mostly just passes that to a shell script
Mix.Tasks.OneProject.ImportTranslations
– downloads translations and other internationalization data from a third-party
Mix.Tasks.OneProject.RollingDeploy
– custom deployment that uses something like a 'blue-green' model, but with a load balancer instead of two entirely separate environments/instances
I haven't used entirely 'regular' Elixir script files much at all in any project – to be called via elixir some-script.exs
. Probably the biggest reason not to, i.e. to just create a custom Mix task or an 'issue script', is that custom Mix tasks are nicely documented, i.e. via mix help
and mix help some-custom-mix-task
, and I really like working at a REPL (iex
).