DEV Community

Camilo
Camilo

Posted on

Code Organization for an Elixir Endpoint

This is just my thoughts on how I would organize code in an endpoint. Let's have an example addresses endpoint that will handle CRUD operations for customer addresses in a store.

For brevety we will only focus on a simple query that returns all the addresses for a given customer id.

The structure is based on CRC and some loose interpretation of the concepts in Designing Elixir Systems with OTP

Directory Structure

├── addresses
│   ├── endpoints.ex
│   ├── metrics.ex
│   ├── repo
│   │   ├── commands.ex
│   │   └── queries.ex
│   ├── requests.ex
│   └── resolvers.ex
├── metrics.ex
├── requests.ex
└── responses.ex
Enter fullscreen mode Exit fullscreen mode

Root Directory

requests.ex

Let's start with the requests.ex file that will standarize the params given to the endpoints and use it as a token (accumulator) to pass between the reducers. This would be a Constructor in CRC.

defmodule Endpoints.Requests do
  alias Endpoints.Metrics

  defstruct [params: %{}, metrics: Metrics.new(), data: %{}, valid?: true]

  def new(metrics, params, data \\ %{}, valid? \\ true) do
    %__MODULE__{metrics: metrics, params: params, data: data, valid?: valid?}
  end

  def new(params) do
    %__MODULE__{params: params}
  end

  def
end
Enter fullscreen mode Exit fullscreen mode

responses.ex

The responses.ex will handle the final result. It will be our Converter in CRC.

defmodule Endpoints.Responses do
  alias Endpoints.Requests

  defstruct [:status, :data, request: nil]

  def new(%Requests{} = request, data, status \\ :ok) do
    %__MODULE__{status: status, data: data, request: request}
  end

  def new(status, data) do
    %__MODULE__{status: status, data: data}
  end

  def ok(data \\ []) do
    new(:ok, data)
  end

  def error(data \\ []) do
    new(:error, data)
  end

  def render(%__MODULE__{} = response) do
    {response.status, response.data}
  end
end
Enter fullscreen mode Exit fullscreen mode

metrics.ex

The metrics.ex is a simple structure that can store the params and functions to send to telemetry and instrumentation services like Prometheus. It's a façade that can standarize and simplify those calls. This can be associated with a Boundary layer, because it interacts with an external component.

defmodule Endpoints.Metrics do

  # Epoch = Start Time
  # Id = Id to Send to the metrics system
  defstruct [:epoch, :id]

  def new(id \\ 0) do
    %__MODULE__{epoch: System.monotonic_time(:microsecond), id: id}
  end

  def count(data, operation, metrics) do
    # Call A metrics system to send the count operation
    {:count, operation, data, metrics.id}
  end

  def count(operation, metrics), do: count([], operation, metrics)

  def error(data, operation, metrics) do
    {:count_error, operation, data, metrics.id}
  end

  def error(operation, metrics), do: error([], operation, metrics)

  @doc """
  Track are for time measurement since it uses the metrics's epoch
  """
  def track(data, operation, metrics) do
    {:track, operation, data, metrics.id, metrics.epoch}
  end

  def track(operation, metrics), do: track([], operation, metrics)
end
Enter fullscreen mode Exit fullscreen mode

Address Directory

endpoints.ex

This is a boundary layer that will receive all the params
from the HTTP or GraphQL Request and will call the other functions and render the final response.

defmodule Endpoints.Addresses.Endpoints do
  alias Endpoints.Addresses.Requests
  alias Endpoints.Metrics

  def get_all_addresses_for_customer_id(customer_id) do
    # initiate metrics here to have a good epoch
    Requests.get_all_addresses_for_customer_id(Metrics.new(), customer_id)
    |> Resolvers.get_all_addresses_for_customer_id()
    |> Responses.render()
  end
end
Enter fullscreen mode Exit fullscreen mode

metrics.ex

These are helper functions to standarize the metrics used inside all the endpoints.

May be these can be automated using macros or if you are adventurous a decorator

defmodule Endpoints.Addresses.Metrics do
  alias Endpoints.Metrics

  def init_address_request(metrics), do: Metrics.count("INIT_ADDRESS_REQUEST", metrics)
  def init_address_request(_data, metrics), do: init_address_request(metrics)

  def count_address_found(metrics), do: Metrics.count("OK_ADDRESS_FOUND", metrics)
  def count_address_found(_data, metrics), do: count_address_found(metrics)

  def count_address_not_found(metrics), do: Metrics.count("OK_ADDRESS_NOT_FOUND", metrics)
  def count_address_not_found(_data, metrics), do: count_address_not_found(metrics)

  def track_address_get(data, metrics): Metrics.track(data, "TRACK_ADDRESS_GET", metrics)
  def track_address_get(metrics), do: track_address_get([], metrics)
end
Enter fullscreen mode Exit fullscreen mode

requests.ex

The address request is a Constructor that will standarize params and return a new Request struct.

Maybe it can validate the params too and see if it valid.

defmodule Endpoints.Addresses.Requests do
  alias Endpoints.Requests

  def get_all_addresses_for_customer_id(metrics, customer_id) do
    Requests.new(metrics, %{customer: %{id: customer_id}})
  end

end
Enter fullscreen mode Exit fullscreen mode

resolvers.ex

The resolver is the one who orchestates queries, requests and responses.

defmodule Endpoints.Addresses.Resolvers do
  alias Endpoint.Responses
  alias Endpoints.Addresses.Requests
  alias Endpoints.Addresses.Repo.Queries
  alias Endpoints.Addresses.Metrics

  def get_all_addresses_for_customer_id(%Requests{} = request) do
    metrics = request.metrics
    Metrics.init_address_request(metrics)
    case Queries.addresses(customer: request.params.customer.id) do
      [] ->
        Metrics.count_address_not_found(metrics)
        |> Metrics.track_address_get(metrics)
        Responses.ok()
      addresses ->
        Metrics.count_address_found(metrics)
        Metrics.track_address_get(addresses, metrics)
        Responses.ok(addresses)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Repo Directory

These are two files that are using CQRS to store queries.

queries.ex

defmodule Endpoints.Addresses.Repo.Queries do
  import Ecto.Query
  use Ecto.Repo

  def addresses(customer: id) do
    from(a in Address,
      where: a.customer_id == ^id
    )
    |> Repo.all()
  end
end
Enter fullscreen mode Exit fullscreen mode

commands.ex

defmodule Endpoints.Addresses.Repo.Commands do
end
Enter fullscreen mode Exit fullscreen mode

Conclusion

These are just some thoughts about code organization. I tried to follow CRC and apply different layers of code organization.

Thanks.

Top comments (0)