Don't want to miss out on Elixir news? Subscribe to ElixirPulse!

The Modifications I Make to Every New Phoenix Project

Bottom line: I make all my database keys UUIDs and add some dependencies, configuration, and a custom schema file to tie it all together. I’ve taken all of these modifications and created a new generator, mix phx.new.john_elm_labs (available on Hex) which will auto-magically set up a new project with these defaults.

You can see the project on GitHub

Install it with mix archive.install hex phx_new_john_elm_labs. Then run mix phx.new.john_elm_labs

The Problem with mix phx.new

Every single time I create a new Phoenix project, I find myself making the same modifications over and over again.

I always want to use UUID/binary IDs in my projects. The generators use integer IDs by default.

The migration & schema generators default to utc_datetime. I always want to use utc_datetime_usec in order to keep microseconds in my DateTimes.

Even when migrations are set up with utc_datetime_usec, I need to set that option on all of my schemas.

That means I create a new schema definition that wraps Ecto.Schema with the folllowing defaults:

@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@timestamps_opts [type: :utc_datetime_usec]

Next, I always add Credo, Styler, and mix test.watch to any new project. These are absent from projects created with mix phx.new.

Read on to see why I make these modifications specifically.

UUID vs Integer ID

This is a fairly well-trodden argument and I won’t re-hash the whole thing here. The bottom line is that UUIDs are far more preferable in modern systems. Given Elixir / Erlang’s propensity for running distributed systems, UUIDs are the natural choice.

Despite this, the Elixir generators default to integer IDs. I always find myself forgetting to switch to binary IDs when I start a new project. For this reason, I included it in my new generator.

Config Changes

New Phoenix projects come with the following config in config/config.exs:

config :my_app,
  ecto_repos: [MyApp.Repo],
  generators: [timestamp_type: :utc_datetime]

This makes generators such as mix phx.gen.schema use :utc_datetime in the inserted_at and updated_at fields.

The problem with utc_datetime is that it truncates your DateTimes to the seconds value. This is not only annoying (since now you need to call DateTime.truncate(datetime, :second) on your value before you insert them, but it is also less precise.

Ordering database records that are entered in the same second is impossible with values truncated at the seconds level, rather than microseconds.

The fix is to change utc_datetime to utc_datetime_usec. However, I frequently forgot to do this and only realized it when it bit me later on down the road.

To get ahead of it, I added this change to the new generator.

In addition to the generators, I also added the following config:

config :my_app, MyApp.Repo, migration_primary_key: [type: :binary_id]
config :my_app, MyApp.Repo, migration_foreign_key: [type: :binary_id]
config :my_app, MyApp.Repo, migration_timestamps: [type: :utc_datetime_usec]

This sets our migration primary key and foreign key types to UUID by default. It also sets our migration timestamps to utc_datetime_usec. This means when we create migrations, we don’t need to remember to specify the type of the foreign key. We also don’t need to remember to specify binary_id when we are referencing another table with references/3.

Custom Schema

In order for the migration config changes to be reflected in the application, we need to add the following to every schema:

@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@timestamps_opts [type: :utc_datetime_usec]

This is tedious and hard to remember to do whenever a new schema is created.

What I do instead is create a new Schema definition that wraps Ecto.Schema:

defmodule MyApp.Schema do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema

      @primary_key {:id, :binary_id, autogenerate: true}
      @foreign_key_type :binary_id
      @timestamps_opts [type: :utc_datetime_usec]
    end
  end
end

Then, in your schemas, you can replace use Ecto.Schema with use MyApp.Schema.

These options make Ecto autogenerate binary IDs and reference other tables using binary IDs. These changes reflect the changes we made to our migrations. Finally, we add some @timestamps_opts to specify our schemas should use utc_datetime_usec. Now, when we write timestamps() in our schemas, the proper type is used.

Run mix phx.gen.auth

The final step I take is to run mix phx.gen.auth Accounts User users --binary-id

If you’re unfamiliar, Phoenix can stand up an entire authorization system out of the box with a single command.

If you would like to use phx.gen.auth in your new application, the generator spits out the proper command for you to do so once it’s complete.

It looks a little bit like:

cd my_app && mix phx.gen.auth Accounts User users --binary-id

New Dependencies

Credo informs you of stylistic mistakes and is a must for any Elixir CI pipeline. Installing it locally lets you catch issues and apply consistent rules to the codebase.

Styler is like Credo on steroids. Not only does it recommend style changes, it fixes them automatically. It’s essential - especially when working with teams. Send PR comments of “style preferences” to the wind and focus on what matters. In addition to installing Styler, you also need to add it to your formatter plugins in .formatter.exs. This is kind of tedious, so I automated it with the generator.

mix test.watch is like a superpower for Test Driven Development. It uses the file watcher to automatically re-run your tests when you save a file. I can’t imagine writing tests without it. This generator goes one step further by setting the default MIX_ENV of the dependency to test. By default, it runs in dev.

In Conclusion

If you like what you see, install the generator and give it a spin:

mix archive.install hex phx_new_john_elm_labs

Then run: mix phx.new.john_elm_labs

Happy Hacking!