codomari · phoenix · routing · api · manifest · handler · strucutre

Codomari #3 (code re-structure)

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:

phoenix app struct

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:

phoenix app struct modified

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:

  1. mix.exs unchanged
  2. removed codomari_backend_web module
  3. moved all stuff from _web to codomari_backend
  4. refactored module names to be under CodomariBackend namespace
  5. renamed controllers to handlers and kept one handler per file

After this modifications main files to check are:

  1. codomari_backend/router/router.ex
  2. 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:

  1. cognitively hard to read
  2. hard to code review - if file has many changes in many places then diff will be longer too
  3. one controller imports many dependencies which may have side effects and etc issues
  4. 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!