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)