Phoenix π¦©
All Elixir conventions apply to Phoenix but working with a framework adds another superset of conventions to follow.
Naming
-
Suffix each project root name with
-web
or-api
depending on the project type.# Bad mix phx.new project_name mix phx.new project_name --live mix phx.new project_name --no-html --no-webpack # Good mix phx.new project-name-web --module ProjectName --app project_name mix phx.new project-name-web --module ProjectName --app project_name --live mix phx.new project-name-api --module ProjectName --app project_name --no-html --no-webpack
-
Use
snake_case
for both database tables and columns. -
Use plural for database tables and singular for column names.
+---------------------------+ | campaign_locations | +-------------+-------------+ | id | ID | | name | STRING | | location_id | FOREIGN KEY | | updated_at | DATETIME | +-------------+-------------+
-
For many-to-many associations, use plural for the joined tables
create table(:posts) do add :title, :string add :body, :text timestamps() end create table(:tags) do add :name, :string timestamps() end # Bad create table(:post_tags, primary_key: false) do add :post_id, references(:posts) add :tag_id, references(:tags) end # Good create table(:posts_tags, primary_key: false) do add :post_id, references(:posts) add :tag_id, references(:tags) end
-
Use
predicate-like
name for boolean database columns.# Bad alter table("users") do add :enabled, :boolean, null: false end # Good alter table("users") do add :is_enabled, :boolean, null: false end
Release
Prefer releasing the application with mix release
or distillery
instead of mix mode
.
Environment Configuration
-
Place all environment variables in
config/prod.secret.exs
(mix mode) orconfig/runtime.exs
(mix release mode with Elixir ~> 1.11). -
Use
System.fetch_env!
to access environment variables inconfig/prod.secret.exs
orconfig/runtime.exs
.config :new_relic_agent, app_name: System.fetch_env!("NEW_RELIC_APP"), license_key: System.fetch_env!("NEW_RELIC_LICENSE")
Since this file is loaded on application boot, it will fail if an environment variable is missing i.e. fast-fail strategy.
Project Structure
Follow the default structure from the Phoenix guide.
βββ _build
βββ assets
βββ config
βββ deps
βββ lib
β βββ project_name
β βββ project_name_web
β βββ project_name.ex
β βββ project_name_web.ex
βββ priv
βββ test
Business Domain
-
The
/project_name
directory hosts all business domains. -
Use Context to decouple and isolate the systems into manageable and independent parts.
-
Prefer to use contexts every time, even if that context has a single model.
For example, we could put the
User
in theAccounts
contextβββ project_name βΒ Β βββ games βΒ Β βΒ Β βββ heroes.ex # Contains all Game Hero Repo actions (eg: list_all, get_by_name) βΒ Β βΒ Β βββ items.ex # Contains all Game Item Repo actions (eg: list_all, get_by_slug) βΒ Β βΒ Β βββ query βΒ Β βΒ Β βΒ Β βββ hero_query.ex # Contains all Game Hero Queries βΒ Β βΒ Β βΒ Β βββ item_query.ex # Contains all Game Item Queries βΒ Β βΒ Β βββ schema βΒ Β βΒ Β βββ hero_schema.ex # Contains Game Hero Schema and all Game Hero Changesets βΒ Β βΒ Β βββ item_schema.ex # Contains Game Item Schema and all Game Item Changesets βΒ Β βββ games.ex # Contains all Game Context Repo action (eg: list_all_items, list_all_heroes, get_hero_by_name, get_item_by_slug) βΒ Β βββ accounts βΒ Β βΒ Β βββ users.ex # Contains all Accounts User Repo actions (eg: list_all, get_by_username) βΒ Β βΒ Β βββ query βΒ Β βΒ Β Β Β βββ user_query.ex # Contains all Accounts User Queries βΒ Β βΒ Β βββ schema βΒ Β βΒ Β Β Β βββ user_schema.ex # Contains Accounts User Schema and all Accounts User Changesets βΒ Β βββ accounts.ex # Contains all Account Context Repo action (eg: list_all_users,get_user_by_username)
Web
-
The
/project_name_web
directory hosts all web-related parts. -
When adding βnon-conventionalβ directories in
/project_name_web
, use the plural for the directory nameβββ project_name_web β βββ params β βββ plugs
LiveView
-
Adding
_live
suffix to the folder and the live module# less preferred βββ live β βββ verification β βββ show.ex # preferred βββ live β βββ verification_live β βββ show_live.ex
-
Adding
_component
suffix toLiveView Component
for bothLiveView Module
andLiveView template
-
Prefer to keep all LiveView components inside the
components
folder.# less preferred βββ live β βββ verification_live β βββ item.ex β βββ show_live.ex βββ templates β βββ verification β βββ item.html.leex β βββ show.html.html.leex βββ views βββ verification_view.ex # preferred βββ live β βββ verification_live β βββ components β βββ item_component.ex β βββ show_live.ex βββ templates β βββ verification β βββ components β βββ item_component.html.leex β βββ show.html.html.leex βββ views βββ verification_view.ex
-
Keep all LiveView templates
(*.html.leex)
under the/templates
folder instead of the/live
folder, usePhoenix.View.render/3
to render a specific LiveView template.# project-name-web/lib/project_name_web/live/verification_live/show_live.ex @impl true def render(assigns) do Phoenix.View.render(VerificationView, "show.html", assigns) end # project-name-web/lib/project_name_web/live/verification_live/components/item_component.ex @impl true def render(assigns) do Phoenix.View.render(VerificationView, "components/item_component.html", assigns) end
Non-conventional Directories
-
When adding βnon-conventionalβ directories in
/lib
, use theproject_name
as a prefix and singular for the directory nameβββ lib β βββ project_name_mail β βββ project_name_worker
Database
-
Prefer the expressions syntax for Ecto queries.
# Least preferred from(p in Payment, where: p.status == "pending") # Preferred Payment |> where([p], p.status == "pending")
Database Migration
-
Name the migration files more elaborately. For table migrations, suppose you add/drop any column to/from a table then, keep both column_name and table_name in migration name. The name of the migration name (e.g.
DropSurnameFromUsers
). For migrations with multiple actions, group the column names and table names in the class name. So the general template is[ChangeMade]+To/From+[TableNamePlural]
# Bad defmodule ProjectName.Repo.Migrations.DropSurnameColumn use Ecto.Migration def change alter table(:users) do remove :surname end end end # Good defmodule ProjectName.Repo.Migrations.DropSurnameFromUsers use Ecto.Migration def change alter table(:users) do remove :surname end end end defmodule ProjectName.Repo.Migrations.DropSurnameNicknameFromUsers use Ecto.Migration def change alter table(:users) do remove :surname remove :nickname end end end
-
For data migration, use a descriptive name.
# Bad defmodule ProjectName.Repo.Migrations.UpdateWebsite use Ecto.Migration def change # your changes to DB here end end # Good defmodule ProjectName.Repo.Migrations.UpdateWebsiteColumnOfUsers use Ecto.Migration def change # your changes to DB here end end
-
Prefer to use multiple migration directories to separate long-running migrate tasks.
βββ priv βΒ Β βββ repo βΒ Β βββ migrations # run automatically βΒ Β βββ manual_migrations # run manually