Suggestion: Clearer Separation Between App Code and Framework Code

Hi Phoenix community!

First off, huge thanks for an amazing framework - Phoenix has been a joy to work with. I have a small UX suggestion that I think could improve developer experience around code clarity.

The Current Confusion

Right now when I write:

defmodule MyApp.SomeLive do
  use MyAppWeb, :live_view
  # ...
end

This injects imports like:

  • import MyAppWeb.Gettext
  • import MyAppWeb.CoreComponents
  • import MyAppWeb.Router.Helpers

As a developer, it’s really hard to tell what’s my actual business logic vs framework-generated code that happens to use my app’s namespace. When I see MyAppWeb.Gettext, my brain thinks “oh, this is code I wrote” but it’s actually just a Phoenix framework wrapper.

File Navigation Confusion

This confusion extends beyond just imports. When browsing my repo directory tree, I often click into files like lib/my_app_web/gettext.ex or lib/my_app_web/telemetry.ex thinking “let me understand this code I wrote,” only to realize it’s boilerplate framework code. Then I have to back out and continue looking for my actual application logic. It’s a small friction, but it adds up over time when you’re trying to navigate your own codebase efficiently.

Acknowledging the Design Intent

I absolutely recognize that the current approach is thoughtful - having lib/my_app_web/my_app_web.ex as a configuration point gives us flexibility to customize what gets injected. The Phoenix team clearly intended this as a way for developers to have control over their application’s setup.

However, I think there’s room to improve the default developer experience while preserving that flexibility.

:light_bulb: Possible Ideas

Option 1: Opt-in customization

mix phx.new my_app --web-config=custom
# Only then generates the MyAppWeb configuration files
# Default could be cleaner with framework namespaces

Option 2: Different namespace

use Phoenix.App, :live_view  # or Phoenix.Web, :live_view
# Makes it clearer this is framework machinery

Option 3: More explicit imports

use MyAppWeb, :live_view, explicit: true
# Could optionally require manual imports for clarity

Option 4: Separate directory structure

lib/
  my_app/           # My actual business logic
  my_app_web/       # My actual web layer code  
  my_app_generated/ # All framework boilerplate (when customization needed)

Option 5: Configuration-driven generation

# Only generate MyAppWeb.* files when I actually want to customize them
# Default behavior uses clean framework namespaces

The Goal

I’d love a way to easily distinguish:

  • :white_check_mark: MyApp.Users - my actual domain code
  • :white_check_mark: MyAppWeb.UserLive - my actual web layer code
  • :robot: Framework/generated code - Phoenix machinery (kept separate until customization needed)

The idea would be preserving the powerful customization capabilities for those who need them, while providing a cleaner default experience for those who don’t.

Has anyone else experienced this confusion? Are there existing patterns I’m missing that make this clearer?

Thanks for considering! Love to hear everyone’s thoughts on this. :folded_hands:

2 Likes

I’m not sure I agree with the resolutions drawn from the stated problem. Yes phoenix generates code, which is boilerplate. But that doesn’t mean that code can simply become “phoenix code” and we’d be good. Personally I even tend to think of phoenix generators as essentially a separate project to phoenix because the constraints around what they do are vastly different.

First of all, a good chunk of the “boilerplate” are not actually related to phoenix itself.

  • MyApp.Repo:ecto
  • MyApp.Mailer:swoosh
  • MyAppWeb.Gettext:gettext
  • MyAppWeb.Telemetry:telemetry / :telemetry_poller

These do not exist because phoenix needs or controls them. Many of them don’t even have explicit integrations with phoenix, but exist plain side-by-side as generated. Most can also be disabled with flags in the generator. The reason for them to be included is because from a complete phoenix project people expect those libraries to be setup and working after creating the project. And I’d ask if you’d put Ecto.Repo in the same bucket of “boiler plate I don’t want to see”, given it imo falls in the same category, we just use it more often and more explicitly.

Generated code is also a learning tool, so those files explicitly exist in conventional places, where you’d also want to put them if you’d setup your phoenix project from scratch integrating those mentioned dependencies on your own.

Allowing for customization is just a sideeffect to “we generate this for you as a starting point, but really it has nothing to do with phoenix itself”.

For ecto there even is phoenix_ecto, which does handle all the integrations between phoenix and ecto, which can be hidden away in a library.

There’s however still also a handful of files setup that are not plain external. However even those are meant to be examples of how you can structure a phoenix setup meant to be adjusted to all the additional concerns you bring in. You will likely bring in other components besides the one phoenix generators use, you might import some more commonly used plugs for controllers. You’ll want to track more domain specific metrics that phoenix generators wouldn’t be able to know about.

In my time using phoenix I’ve adjusted and customized all of the files you get generated in your project. Maybe not all of them in each project, but they’re certainly not there to be admired unchanged – if they would that code would live in library code.

So generally I’d suggest going another direction here. If you encounter a file you don’t know what it does or why it exists – delete it or learn what it might provide. If you encounter a file you know why it’s there, but think you could do it better – do so. If you’re not happy with the location of the file – move it.

6 Likes

In general the agreed-upon consensus is that “Phoenix is not your app” i.e. everything you need for your app gets injected at generating-the-project phase – and then you keep adding stuff.

That being said, I have wanted, for a long time, a way to indeed cleanly separate what is 100% my project and what are framework artifacts – be they Phoenix’s or other adjacent libraries’ as @LostKobrakai pointed out.

That would be what I go for. If you are willing to put the elbow grease that would be the right thing to do IMO. Elixir compiler cares not where exactly your files are as long as you include them in the paths to compile in mix.exs. I have also mulled over introducing some tags in all framework files i.e. special comments with JSON or XML tags or just Elixir module attributes but it never got anywhere as life and career kept getting in the way.

But you are not alone. I am dreaming of an entirely alternative directory i.e. gen/ that contains sources that must be regenerated when something else changes i.e. a GraphSQL spec would lead to f.ex. an Absinthe file being changed.

People would point out that we have @external_resource for that but it’s not what I have in mind; it’s more like “here, we have this OpenAPI spec JSON / YAML file, it changed, please regenerate the actual sources of the HTTP client that accesses the API” (not just regenerate code invisibly and inject functions at compile time).

2 Likes

A while ago I started to do MyApp.{Library}.… – unconventionally nested in lib/myapp/support though – as a setup for stuff being wrappers or extensions to external libraries like ecto query fragments, custom changeset functions and so on. Stuff I could properly move to an intermediate dependency if I’d want to take on the overhead of maintaining that. Haven’t felt the need much on the web side of phoenix yet. Once there’s some coupling to the current project it imo belong to the current project.

1 Like

This, while fair (and is the way I am coding the Rust part of my SQLite library), is not the same. In my case it’s more like “is this my business logic or is it plumbing that I need”?

Well yes, true. Guess some of us have this nagging feeling of stuff being mixed when they feel it should not.

Granted, that’s just a feeling so I stopped worrying about it; some mild OCD that should be ignored and we should move on with life. Plus, if I can’t explain it well to myself then it’s not really an important consideration, ultimately.

1 Like