Phoenix 🦩

Hero image for 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 the team template for all Phoenix projects.

  • 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) or config/runtime.exs (mix release mode with Elixir ~> 1.11).

  • Use System.fetch_env! to access environment variables in config/prod.secret.exs or config/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.

  • Prefer to keep the context name singular.

    For example, we could put the User in the Account context

    β”œβ”€β”€ project_name
    β”‚Β Β  β”œβ”€β”€ game
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ games.ex # Contains all Game Context actions (eg: save_game_state, load_game_state)
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ 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)
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ queries
    β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ hero_query.ex # Contains all Game Hero Queries
    β”‚Β Β  β”‚Β Β  β”‚Β Β  └── item_query.ex # Contains all Game Item Queries
    β”‚Β Β  β”‚Β Β  └── schemas
    β”‚Β Β  β”‚Β Β      β”œβ”€β”€ hero.ex # Contains Game Hero Schema and all Game Hero Changesets
    β”‚Β Β  β”‚Β Β      └── item.ex # Contains Game Item Schema and all Game Item Changesets
    β”‚Β Β  β”œβ”€β”€ account
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ users.ex # Contains all Accounts User Repo actions (eg: list_all, get_by_username)
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ queries
    β”‚Β Β  β”‚Β Β  β”‚Β   └── user_query.ex # Contains all Accounts User Queries
    β”‚Β Β  β”‚Β Β  └── schemas
    β”‚Β Β  β”‚Β Β  Β Β   └── user.ex # Contains Accounts User Schema and all Accounts User Changesets
    

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 to LiveView Component for both LiveView Module and LiveView 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, use Phoenix.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 the project_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