30
votes

Trying to create an app with Elixir + Phoenix, that would be able to handle both "browser" and "api" requests to handle its resources.

Is it possible to do it without having to do something like that :

scope "/", App do
  pipe_through :browser

  resources "/users", UserController
end

scope "/api", App.API as: :api do
  pipe_through :api

  resources "/users", UserController
end

which would mean having to create two controllers, which might have the same behavior, except that it will render HTML with the browser pipeline and, say JSON, for the api pipeline.

I was thinking maybe something like the Rails respond_to do |format| ...

2

2 Answers

22
votes

I wouldn't recommend it (I would recommend having two controllers and move your logic into a different module that is called by both controllers) but it can be done. You can share a controller, but you still need a separate pipeline to ensure the correct response type (html/json) is set.

The following will use the same controller and view, but render json or html depending on the route. "/" is html, "/api" is json.

Router:

defmodule ScopeExample.Router do
  use ScopeExample.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", ScopeExample do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
  end

  scope "/api", ScopeExample do
    pipe_through :api # Use the default browser stack

    get "/", PageController, :index
  end
end

Controller:

defmodule ScopeExample.PageController do
  use ScopeExample.Web, :controller

  plug :action

  def index(conn, params) do
    render conn, :index
  end
end

View:

defmodule ScopeExample.PageView do
  use ScopeExample.Web, :view

  def render("index.json", _opts) do
    %{foo: "bar"}
  end
end

You can also share the router and have everything served by the same route if you use a router like:

defmodule ScopeExample.Router do
  use ScopeExample.Web, :router

  pipeline :browser do
    plug :accepts, ["html", "json"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
  end


  scope "/", ScopeExample do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
  end
end

You can then specify the format using ?format=json at the end of the url - I would recommend going with different urls for your API and site however.

30
votes

As Gazler said, you are probably best served by having separate pipelines, but something like this can be pleasantly done with pattern matching on the same controller actions:

def show(conn, %{"format" => "html"} = params) do
  # ...
end

def show(conn, %{"format" => "json"} = params) do
  # ...
end

Or if the function bodies are the same, and you would only like to render a template based on the accept headers, you can do:

def show(conn, params) do
  # ...

  render conn, :show
end

Passing an atom as the template name will cause phoenix to check the accept headers and render the .json or .html template.