Most idiomatic way to render data with Phoenix and Ecto

In the JSON API I’m building I am returning resources which are often the denormalized version of my database schemas. I’m wondering what the most idiomatic approach is for this, transforming the data via views in Phoenix, creating a custom schema, or doing the manipulation via the query?

Here’s an example. In my database schema, I have a Plan which has two subtypes, MatchPlan and FixedPlan.

schema "frequency" do
  field :name, :string
  has_many :plan, Plan
end

end
schema "plan" do
  belongs_to :frequency, Frequency
  has_one :match_plan, MatchPlan
  has_one :fixed_plan, FixedPlan
end

schema "match_plan" do
  field :percentage, :decimal
  field :limit
  belongs_to :plan, Plan
end

schema "fixed_plan" do
  field :amount
  belongs_to :plan, Plan
end

This is too abstracted for my API to return, I’d like to return a resource like this:

{
  "id": 1,
  "object": "plan",
  "frequency": "monthly",
  "plan_type": "match",
  "limit": 100,
  "amount": null
}

Here’s the approaches I see:

Pass a preloaded struct to a view like-so. The downside here is there’s a lot null checking and the transformation logic can get nasty. I have much more complex data structures than this too.

def render("show.json", %{match_plan: match_plan, fixed_plan: fixed_plan, frequency: frequency} = plan) do
  %{
    id: plan.id,
    object: "plan",
    plan_type: if match_plan, do: "match_plan", else: "fixed_plan",
    frequency: frequency.name,
    limit: if match_plan, do: match_plan.limit, else: nil,
    amount: if fixed_plan, do: fixed_plan.amount, else: nil
  }
end

Second, create a custom schema. I know this could be done by creating a view in the database and mapping a schema to it. But I’d rather not have my business logic at the database level.

Lastly, I can use a query. This method is easy, but might not support all the transformations I need to do and it doesn’t seem like the right place for the business logic.

from p in Plan,
  join: f in assoc(p, Frequncy),
  left_join: mp in assoc(p, MatchPlan),
  left_join: fp in assoc(p, FixedPlan),
  select: %{
    id: p.id,
    object: "plan",
    frequency: f.name,
    plan_type: fragment("IF ? IS NULL THEN 'match' ELSE 'fixed' END", mp.id),
    limit: mp.limit,
    amount: fp.amount
  }

What do you guys think? Is there another option?

Maybe You can put the rendering logic inside the views…

I usually do this in controller

  def index(conn, _params) do
    games = Gaming.list_games
    render(conn, "index.json", games: games)
  end

in the context…

  def list_games(params \\ []) do 
    limit = params[:limit] || @default_limit
    offset = params[:offset] || 0
    
    Game
    |> limit(^limit)
    |> offset(^offset)
    |> order_by([asc: :id])
    |> preload([:black_player, :white_player])
    |> Repo.all
  end

and finally in the view

  def render("index.json", %{games: games}), do: %{games: games_json(games)}
  
  def render("show.json", %{game: game}), do: %{game: game_json(game)}
  
  # PRIVATE
  
  defp games_json(games), do: games |> Enum.map(&game_json(&1))
  
  defp game_json(game) do 
    %{
      id: game.id, 
      game_info: game.game_info, 
      black_player: player_json(game.black_player), 
      white_player: player_json(game.white_player)
    }
  end
  
  defp player_json(player) do
    %{id: player.id, name: player.name}
  end

But those days I would choose absinthe as a graphql api, then for each field You can use a specific resolver function… I set up search, connections, virtual root field, node interface and cursor, pageInfo, edges, node etc.

Because I am using react on the frontend, absinthe plays nicely with relay modern.

Thanks for the perspective! I’m using Swift on the front-end.