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 inlib/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