View Source Setting up a Phoenix project

This guide walks through a typical process for setting up a Phoenix project. It assumes you've read the How It Works and Getting Started guides.

Imagine creating an entire new application simply by adding a Route and a single LiveView to an existing Elixir project.

This is in fact the vision that drove the creation of Uniform. The overhead of experimenting with new apps becomes extremely low, incentivizing the team to try out ideas without being inhibited by initial setup time.

setting-up-your-blueprint-module

Setting up your Blueprint module

Below is an example Blueprint module for ejecting the files common to Phoenix applications.

defmodule MyBaseApp.Uniform.Blueprint do
  use Uniform.Blueprint, templates: "lib/my_base_app/uniform/templates"

  base_files do
    cp_r "assets"
    cp_r "priv/static"

    file "lib/my_base_app_web.ex"
    file "config/**/*.exs"
    file "test/support/*_case.ex"
  end

  deps do
    # Always eject the `my_base_app` and `my_base_app_web` libraries.
    #
    # Their paths and file contents will replace my_base_app with the ejected
    # app name automatically.
    always do
      lib :my_base_app do
        only [
          "lib/my_base_app/application.ex",
          "lib/my_base_app/repo.ex"
        ]

        # `match_dot: true` so that we eject `priv/repo/.formatter.exs`
        file "priv/repo/**/*.exs", match_dot: true
      end

      lib :my_base_app_web do
        only [
          "lib/my_base_app_web/endpoint.ex",
          "lib/my_base_app_web/gettext.ex",
          "lib/my_base_app_web/router.ex",
          "lib/my_base_app_web/telemetry.ex",
          "lib/my_base_app_web/channels/user_socket.ex",
          "lib/my_base_app_web/templates/layout/app.html.heex",
          "lib/my_base_app_web/templates/layout/live.html.heex",
          "lib/my_base_app_web/templates/layout/root.html.heex",
          "lib/my_base_app_web/views/error_helpers.ex",
          "lib/my_base_app_web/views/error_view.ex",
          "lib/my_base_app_web/views/layout_view.ex"
        ]
      end
    end
  end
end

Let's walk through it step by step.

the-base_files-section

The base_files section

base_files do
  cp_r "assets"
  cp_r "priv/static"

  file "lib/my_base_app_web.ex"
  file "config/**/*.exs"
  file "test/support/*_case.ex"
end

In the base_files section, we specify files that should be ejected in every app. Phoenix apps will typically have CSS and JS assets in the assets directory. They'll also have static files to be served as-is in priv/static. Most of these files will typically be binary (non-text) files. So we assume none of them need to pass through the Code Transformation phase. That's why the first two lines are included.

cp_r "assets"
cp_r "priv/static"

Note that cp_r instructs mix uniform.eject to copy all the directory contents (using File.cp_r!/3).

Phoenix apps typically have a Web module which is used to construct Controllers, Views, Routers, and LiveViews. Since this file is typically in lib/ directly (and not in a sub-directory of lib/), we include it here in the base_files section.

file "lib/my_base_app_web.ex"

For the last two lines below, we'll use wildcard characters to target multiple files. (file accepts a Path.wildcard/2 glob or a concrete path.)

We proceed with the assumption that the ejected app will need the Base Project's configuration files.

file "config/**/*.exs"

Lastly, we include all the custom test cases in the Base Project.

file "test/support/*_case.ex"

using-front-end-javascript-libraries

Using front-end JavaScript Libraries

If you're using a front-end JavaScript library like React.js, you probably don't want to eject all of the contents of assets with every app like this.

Depending on your setup, you may want to put JS files in separate directories for each app and include them in base_files in one of these ways.

# with Code Transformations
base_files do
  file "assets/#{app.name.underscore}/**/*.{js,ts}"
end
# without Code Transformations
base_files do
  cp_r "assets/#{app.name.underscore}"
end

the-deps-section

The deps section

In the deps section, we put both lib :my_base_app and lib :my_base_app_web inside always do so that their contents are always ejected without having to specify lib_deps: [:my_base_app, :my_base_app_web] in the uniform.exs manifest of every app.

deps do
  always do
    lib :my_base_app do
      # ...
    end

    lib :my_base_app_web do
      # ...
    end
  end
end

For :my_base_app, we use an only instruction to exclude all files in lib/my_base_app and test/my_base_app except for application.ex and repo.ex.

lib :my_base_app do
  only [
    "lib/my_base_app/application.ex",
    "lib/my_base_app/repo.ex"
  ]

  file "priv/repo/**/*.exs", match_dot: true
end

You may have a setup that requires you to add more files, such as mailer.ex.

Note that we also include all of the Repo's migrations and seeds scripts with a glob: priv/repo/**/*.exs. Both that and the match_dot option are passed to Path.wildcard/2 under the hood. match_dot: true ensures that priv/repo/.formatter.exs is ejected so that the ejected codebase is formatted properly.

For :my_base_app_web, we also use an only instruction to only include relevant files.

lib :my_base_app_web do
  only [
    "lib/my_base_app_web/endpoint.ex",
    "lib/my_base_app_web/gettext.ex",
    "lib/my_base_app_web/router.ex",
    "lib/my_base_app_web/telemetry.ex",
    "lib/my_base_app_web/channels/user_socket.ex",
    "lib/my_base_app_web/templates/layout/app.html.heex",
    "lib/my_base_app_web/templates/layout/live.html.heex",
    "lib/my_base_app_web/templates/layout/root.html.heex",
    "lib/my_base_app_web/views/error_helpers.ex",
    "lib/my_base_app_web/views/error_view.ex",
    "lib/my_base_app_web/views/layout_view.ex"
  ]
end

the-phoenix-router

The Phoenix Router

A simple way to set up your Phoenix.Router is to put the routes for all of your apps in a single router. Then, use Eject Fences and Uniform.Blueprint.modify/2 to transform the router upon ejection.

Let's look at an example. We'll explain each part below.

defmodule MyBaseAppWeb.Router do
  use MyBaseAppWeb, :router

  pipeline :browser do
    # ...
  end

  # uniform:app:some_app
  scope "/some-app", SomeAppWeb do
    pipe_through :browser

    get "/widgets", WidgetController, :index
    get "/widgets/new", WidgetController, :new
    post "/widgets/new", WidgetController, :create
    get "/widgets/:widget_id", WidgetController, :show
  end
  # /uniform:app:some_app

  # uniform:app:another_app
  scope "/another-app", AnotherAppWeb do
    pipe_through :browser

    get "/posts", PostController, :index
    get "/posts/new", PostController, :new
    post "/posts/new", PostController, :create
    get "/posts/:post_id", PostController, :show
  end
  # /uniform:app:another_app
end

You'll want to structure the router for reuse by including any pipelines (e.g. :browser or :api) that your apps will need.

pipeline :browser do
  # ...
end

Next, add scopes for each app, wrapped in Eject Fences. This ensures ejected routers will only contain routes related to the ejected app.

# uniform:app:some_app
scope "/some-app", SomeAppWeb do
  # ...
end
# /uniform:app:some_app

You'll need to add controllers/views/etc inside of the app's lib directory.

For example, SomeAppWeb.WidgetController should be in lib/some_app.

(Probably at lib/some_app/controllers/widget_controller.ex.)

Prefix the paths of each scope with /app-name (like /some-app above) so that each app has a predictable, separated URL structure when running the Base Project locally.

As a last step, we need to remove the /app-name path prefix during ejection. We can do this with modify.

defmodule MyBaseApp.Uniform.Blueprint do
  use Uniform.Blueprint

  modify "lib/my_base_app_web/router.ex", fn file, app ->
    String.replace(
      file,
      "scope \"/#{app.name.hyphen}\"",
      "scope \"/\""
    )
  end
end

With this modifier, the following code

scope "/some-app", SomeAppWeb do

Changes to

scope "/", SomeAppWeb do

In the ejected codebase.

This method is a great starting point. Before you reach dozens of apps, you may want to consider other methods that allow you to define routes in a separate file per app.

internal-pages

Internal Pages

We encourage running all apps simultaneously via the Base Project as your development environment. In such a setup, it can be useful to add other pages to the Base Project that aren't intended to be ejected with any app.

For example, you might add a page that catalogs and links to your various apps. We recommend adding these routes and wrapping them all in # uniform:remove Eject Fences as in the example above.

# uniform:remove
scope "/", SomeAppWeb do
  pipe_through :browser

  get "/internal-team-page", InternalTeamController, :index
end
# /uniform:remove

eject-fences-everywhere

Eject Fences Everywhere!

There are other files which are central for running Elixir apps.

Similarly to the Phoenix Router, we recommend that you add the code required by each of your apps and Lib Dependencies to all of these files. Then, use Eject Fences to selectively remove code during ejection.

Let's examine what this might look like for application.ex, mix.exs, and config/*.exs files.

application

Application

Your Application file at lib/my_base_app/application.ex is a critical piece of Elixir applications since it's used to start processes and supervisors at the start of the application.

Here's what an example Application file would look like with Eject Fences applied.

defmodule MyBaseApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      MyBaseAppWeb.Endpoint,
      {Phoenix.PubSub, name: MyBaseApp.PubSub},
      MyBaseAppWeb.Presence,
      MyBaseAppWeb.Telemetry,

      # uniform:lib:my_first_data_lib
      MyFirstDataLib.Repo,
      # /uniform:lib:my_first_data_lib

      # uniform:lib:my_second_data_lib
      MySecondDataLib.Repo,
      MySecondDataLib.Vault,
      # /uniform:lib:my_second_data_lib

      # uniform:remove
      SomeDevelopmentOnlyDB.Repo,
      # /uniform:remove

      # uniform:mix:oban
      {Oban, ...},
      # /uniform:mix:oban
    ]

    # ...
  end
end

Notice that code which should always be ejected does not get surrounded by Eject Fences.

mix-exs

mix.exs

Some dependencies require modifying mix.exs. For example, the exq Hex package says to add :exq to application in mix.exs.

def application do
  [
    applications: [:logger, :exq],
    # ...
  ]
end

But what if only some of your apps require exq? Wrap the exq-specific code in Eject Fences, and it will only be included when exq is required.

def application do
  [
    applications: [
      :logger,
      # uniform:mix:exq
      :exq
      # /uniform:mix:exq
    ],
    # ...
  ]
end

You don't need to use Eject Fences in deps

Note that removing deps from the deps section of mix.exs is automatic, so this would not be required.

# ❌ There is no need to wrap individual deps in eject fences
defp deps do
  [
    # uniform:mix:jason
    {:jason, "~> 1.0"}
    # /uniform:mix:jason
  ]
end

config-files

Config Files

Many dependencies also require configuration. Apply Eject Fences in your configuration files for the same result.

# uniform:mix:guardian
config :my_base_app, MyBaseApp.Guardian,
       issuer: "my_base_app",
       secret_key: ...
# /uniform:mix:guardian