In this post I’m writing about how I’ve restructured app from generated Phoenix structure.
When we generate app using mix phx.new app_name
it will generate structure like:
So as we can see routing and controllers are in _web
folder which a bit unusual if we came from other frameworks.
Cause we did not requested for 2 modules or why routing and controllers are not in folder without _web
suffix?
So I’ve moved everything under one module CodomariBackend
:
We know that our backend will not do rendering, but based on that we may have 404 or 500 pages I’ve kept some views and routings.
Result:
mix.exs
unchanged- removed
codomari_backend_web
module - moved all stuff from
_web
tocodomari_backend
- refactored module names to be under
CodomariBackend
namespace - renamed controllers to handlers and kept one handler per file
After this modifications main files to check are:
codomari_backend/router/router.ex
codomari_backend/handlers/...
codomari_backend/router/router.ex
In router main things to mention is that I’m having 2 scopes: /
(root) and /api
Inside this scopes I’m defining per handler per method route.
So there is no controller, there are only handlers defined to routes.
Reason of approach is to explicitly define where, what route must be handled by which handler.
defmodule CodomariBackend.Router do
use CodomariBackend, :router
alias Handlers.Api.ManifestHandler, as: ManifestHandler
alias Handlers.Public.IndexPageHandler, as: IndexPageHandler
pipeline :api do
plug :accepts, ["json"]
end
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, html: {CodomariBackend.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/api", CodomariBackend do
pipe_through :api
get "/", ManifestHandler, :handle
get "/manifest", ManifestHandler, :handle
end
scope "/", CodomariBackend do
pipe_through :browser
get "/", IndexPageHandler, :handle
end
end
Second reason for this approach is that 1 file per handler makes our handler modules simple and self contained.
Let’s remind: UsersController
having create
, get
, update
, delete
and etc methods “sandwiched” inside one file.
Issues of such structure is obviously long file - which is:
- cognitively hard to read
- hard to code review - if file has many changes in many places then diff will be longer too
- one controller imports many dependencies which may have side effects and etc issues
- we know that Elixir developers love to overload methods by patterns and etc - so controller can grow bigger with private helper methods
That’s why I keep it simple: 1 module = 1 handler
codomari_backend/handlers/api/manifest_handler.ex
defmodule CodomariBackend.Handlers.Api.ManifestHandler do
use CodomariBackend, :controller
@doc """
Returns information about the service.
"""
@spec handle(Plug.Conn.t(), map) :: Plug.Conn.t()
def handle(conn, _params) do
project_data = project_from_mix()
response_data =
[
project: "codomari",
type: "service"
] ++
project_data
json(conn, Jason.OrderedObject.new(response_data))
end
@doc """
Returns the project name and version from the mix project.
"""
@spec project_from_mix() :: [name: String.t(), version: String.t()]
defp project_from_mix do
app = CodomariBackend.MixProject.project()[:app]
version = CodomariBackend.MixProject.project()[:version]
[
name: Atom.to_string(app),
version: version
]
end
end
- as we can see our handler is small, easy to read, our private method is used only in one place so we kept that function inside handler and it does not add complexity to code.
All other files you can get from my repo: https://github.com/num8er/codomari-backend
Talk Soon!