Search code examples
elixirphoenix-frameworkecto

Phoenix : Having a JSON and HTML representation of the same resource


I'm starting to use Phoenix outside of tutorials for a small use case. So far great, but I'm a bit taken aback by the following.

I have a resource "recording" that I want to access at /api/recordings in JSON and at /recording with a template and the result being HTML.

Ideally the Ecto representation would be unique, and even part of the controller would also be shared?

Right now I either have to have 2 resources recordingAPI and recordingHTML, or 2 controllers and 1 resources.

Any exemple out there? I keep finding one or the other, but not something with the :api pipe and the :browser pipe used for the same resource.

Thanks


Solution

  • You can utilize phoenix accepts plug and a combination of two different views/layouts

    # router.ex
    defmodule TestExWeb.Router do
      use TestExWeb, :router
    
      pipeline :browser do
        plug :accepts, ["html"]
        plug :fetch_session
        plug :fetch_flash
        plug :protect_from_forgery
        plug :put_secure_browser_headers
      end
    
      pipeline :api do
        plug :accepts, ["json"]
      end
    
      scope "/", TestExWeb do
        pipe_through :browser
    
        get "/recording", PageController, :index
      end
    
      scope "/api", TestExWeb do
        pipe_through :api
    
        get "/recording", PageController, :index
      end
    end
    

    As you can see, :browser uses :accepts ["html"] while :api uses :accepts ["json"]. You can find this in the private struct of your conn and use it in the controller like this:

    defmodule TestExWeb.PageController do
      use TestExWeb, :controller
    
      def index(%{private: %{phoenix_format: format}} = conn, _params) do
        data = "Hello World"
    
        render(conn, "index.#{format}", data: data)
      end
    end
    

    Now, you just need to tell phoenix how to render your json, html is already taken care of by page.html.eex in layouts, so add the following to your page_view.ex

    defmodule TestExWeb.PageView do
      use TestExWeb, :view
    
      def render("index.json", %{data: data}) do
        %{
          data: data
        }
      end
    end
    

    Two drawbacks with this solution:

    • You need this format snippet in every controller (Maybe you can circumvent this with a plug after you "sent" the response)
    • You're using an internal variable of phoenix