ashkan117

ashkan117

I’m wondering how do people structure their JSON Api’s with Phoenix. Using the blogs example, let’s say I have a blogs view like the following

  def render("show.json", %{blog: blog}) do
    %{
      id: blog.id,
      title: blog.title,
      posts: render_many(blog.posts, PostView, "post.json")
    }

This runs into things like what if posts has its own associations like comments, etc.

In my controller maybe sometimes I don’t care about the associations and other times I do. This leads me to two main problems?

  1. Are there any standards people follow to handle when a view should try and render associations?
  2. Should there be some helper function in Phoenix.View that handles conditionally rendering associations?

For the first problem I’m just curious how people do something like the following

  def show(conn, %{"id" => id}) do
    blog = Blogs.get_blog_with_all(id)
    # assume no associations are preloaded
    render(conn, "show.json", blog: blog)
  end

  def some_action(conn, %{"id" => id}) do
    blog = Blogs.get_blog_with_all(id, preload: [:association_one, :association_two])
    # here I want to include the two associations in my view
    render(conn, "show_with_association_one_and_two.json", blog: blog)
  end

Having a specific view for each combination of association seems tedious.

For the second problem I mean something like having a helper function maybe_render_association that will add the field to the view if the association is loaded or it will not include the field at all. My thinking is we don’t want to return nil in the case that the association is not loaded since it’s not actually nil. That pattern would look like


def render() do
  def render("post.json", %{post: post}) do
    %{
      id: post.id,
      title: post.title,
      body: post.body,
      comments: render_many(post.comments, CommentView, "comment.json")
    }
    # it would be nice if we can have it inside the above map but 
    # since it's a conditional field not sure how
    |> maybe_render_association(post.updated_by, :updated_by, User, "basic.json")
  end
end

First 10 of 10 Posts Switch mode

Hermanverschooten

Hermanverschooten

I never generalize those views.
I keep them specific to the action that renders them.
This keeps the information together and prevents unneeded data going to the client.

FlyingNoodle

FlyingNoodle

The a look at the JSONAPI spec which has “includes” for this.

Be careful though that you take care of proper visibility checks even if you are including a relationship.

ashkan117

ashkan117 OP

Ah I didn’t know that that was part of the spec, that is nice to know.

ashkan117

ashkan117 OP

I can see that. Do you have a particular way to structure that? For example let’s say we have two actions list_blogs and get_blog that our endpoints use. Let’s say that list_blogs doesn’t want to preload created_by but get_blog does. How would you name the templates for the JSON view?

So the controller will delegate out to the view

  # controller
  def list_blogs(conn, params) do
    ...
    render(conn, "index.json", blogs: blogs)
  end

  def show(conn, %{"id" => id}) do
    ...
    render(conn, "show.json", blog: blog)
  end

We can assume that the controller properly preloads the data it needs. Then in the View how do you handle this conditional logic of some fields were preloaded and therefore exist but for other templates they don’t?

  # view file
    def render("index.json", %{blogs: blogs}) do
    %{data: render_many(blogs, __MODULE__, "NO CREATED BY PRELOAD")}
  end

  def render("show.json", %{blog: blog}) do
    %{data: render_one(blog, __MODULE__, "NEEDS CREATED BY PRELOAD.json")}
  end

So far I’ve referencing the same template in both render_many and render_one cases. So I’d call something like "blog.json" but then I run into the case that index doesn’t want to show the preloaded data but show does

LostKobrakai

LostKobrakai

Imo you’re at the “consequences” end of a decision, but you haven’t talked about the decision itself: Why do you need that in the first place? What is the reason for representing the same data in distinct ways. That reason is your key to splitting up the codepaths.

Hermanverschooten

Hermanverschooten

I use the newer syntax, so no longer render_many or render_one, but just returning the map.

def render("index.json", %{blogs: blogs}) do
  for blog <- blogs do
    %{
      id: blog.id,
      title: blog.title
    }
  end
end

def render("show.json", %{blog: blog}) do
  %{
    id: blog.id,
    title: blog.title,
    created_by: blog.who,
    created_at: DateTime.to_is8601!(blog.inserted_at)
  }
end
ashkan117

ashkan117 OP

I believe the reason is that I want a flexible way to preload data which causes some fields to be sometimes present. To present that as a template I think the main two ways are

  1. To have one view template to rule them all
  2. To have one view template per distinct way

The first one sounds better but then I’d need to introduce some utilities to handle the conditional logic which leads me to this type of pattern to handle the fact that if an association is not preloaded, then I don’t want it to show up at all in the response (I don’t want to return nil for a case where we haven’t loaded the data)

def render() do
  def render("post.json", %{post: post}) do
    %{
      id: post.id,
      title: post.title,
      body: post.body,
      comments: render_many(post.comments, CommentView, "comment.json")
    }
    # it would be nice if we can have it inside the above map but 
    # since it's a conditional field not sure how
    |> maybe_render_association(post.updated_by, :updated_by, User, "basic.json")
  end
end

If there’s no better way and these utilities are overall the best approach then I was thinking that maybe it should be built into Phoenix.View

ashkan117

ashkan117 OP

Ah true I guess render_many and render_one are not really needed anymore. I guess in your case how would associated data use these views?

So if a user has many posts and you want to show that for the “user.json” render do you just use another for loop or do you try and refer to the blog views?

Hermanverschooten

Hermanverschooten

Mine usually is an index view that lists all “items”, then a show that takes the id and returns the details for that item, this is in an app where the frontend is in Elm.

And I use distinct views for each. There is seldom any reuse as each page required other data and gets only that data.

I feel that if you want your frontend to dictate what data should be returned, you look into GraphQL.

LostKobrakai

LostKobrakai

I don’t think phoenix should drive such helpers. I’d argue a none preloaded association reaching a view template expecting that association to be preloaded is an error. There’s no definitive answer to if that association is missing because of a bug, because of the intention to skip the key of the association in the json or because of the intention to mask the data not being loaded with an empty value ([] or nil). I’ve seen people argue in favor of all of these, so a generalize solution is likely not the answer.

You can however quite build such a helper for your project and your intentions.

Personally I’d push this concern to other places though. E.g. have PostUser – like a user as shown in relation to posts – vs a UserDetails – user as rendered by e.g. /users/:id. That requires more upfront modeling, but makes code a lot more explicit. You’re not passing around data, which could fit 10+ branches of results – and you hope you get the one you intend – but you pass around data that matches the one or few intented usecases. It also makes change easier, because the approach of a single very dynamic view template works great if all “branches” are kind of supersets of the base case. It gets hairy though once they have conflicting keys. Say eventually you want to mask part of the users as returned with posts, but not mask that for user details. It might feel like duplication at first, but going that route is more maintainable in the long run.

— All posts loaded —

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
mudasobwa
While I am working on the Language Agnostic Code Audit SaaS, which uses MetaAST (spoiler: I am expecting it to be in a good shape for ann...
New

We're in Beta

About us Mission Statement