content
adds Content Negotiation
to any Phoenix App
so you can render HTML and JSON for the same route.
We need to reduce eliminate duplication of effort
while building our App+API so we can ship features much faster.
Using this Plug we are able to build our App (Phoenix Web UI)
and a REST (JSON) API in the same codebase with minimal effort.
A Plug that can be added to any Phoenix App
to render both HTML
and JSON
in the same route/controller
so that we save dev time.
By ensuring that all Web UI
has a corresponding JSON response
we guarantee that everyone has
access to their data in the most convenient way.
Returning an HTML
view for people using the App in a Web Browser
and returning JSON
for people requesting the same endpoint
from a script (or a totally independent front-end)
we guarantee that all features of our Web App
are automatically available in the API.
We have built several Apps and APIs in the past and felt the pain of having to maintain two separate codebases. It's fine for mega corp with hundreds/thousands of developers to maintain a separate web UI and API applications. We are a small team that has to do (a lot) more with fewer resources!
If you are new to content negotiation in general or how to implement it in Phoenix from scratch, please see: dwyl/phoenix-content-negotiation-tutorial
This project is "for us by us". We are using it in our product in production. It serves our needs exactly. As with everything we do it's Open Source so that anyone else can benefit. If it looks useful to you, use it! If you have any ideas/requests for features, please open an issue.
In less than 2 minutes and 3 easy steps you will have content negotiation enabled in your Phoenix App and can get back to building your app!
Add content
to your list of dependencies in mix.exs
:
def deps do
[
{:content, "~> 1.3.0"}
]
end
Then run mix deps.get
.
Open the router.ex
file in your Phoenix App.
Locate the pipeline :browser do
section.
And replace it:
Before:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
After:
pipeline :any do
plug :accepts, ["html", "json"]
plug Content, %{html_plugs: [
&fetch_session/2,
&fetch_flash/2,
&protect_from_forgery/2,
&put_secure_browser_headers/2
]}
end
Don't forget to change the pipeline you just changed inside the scopes. As such, you should change according to the following:
Before:
scope "/", AppWeb do
pipe_through(:browser)
get("/", PageController, :index)
end
After:
scope "/", AppWeb do
pipe_through(:any)
get("/", PageController, :index)
end
Pass the plugs you want to run for html
as html_plugs
(in the order you want to execute them).
Note: the
&
and/2
additions to the names of plugs are theElixir
way of passing functions by reference.
The&
means "capture" and the/2
is the arity of the function we are passing.
We would obviously prefer if functions were just variables like they are in some other programming languages, but this works.
See: https://dockyard.com/blog/2016/08/05/understand-capture-operator-in-elixir
and: https://culttt.com/2016/05/09/functions-first-class-citizens-elixir
Example:
router.ex#L6-L11
In your controller(s),
add the following line to invoke Content.reply/5
which will render HTML
or JSON
depending on the accept
header:
Content.reply(conn, &render/3, "index.html", &json/2, data)
Again, those
&
and/3
are just to letElixir
know whichrender
andjson
function to use.
The Content.reply/5
accepts the following 5 argument:
conn
- thePlug.Conn
where we get thereq_headers
from.render/3
- thePhoenix.Controller.render/3
function, or your own implementation of a render function that takesconn
,template
anddata
as it's 3 params.template
- the.html
template to be rendered if theaccept
header matches"html"
; e.g:"index.html"
json/2
- thePhoenix.Controller.json/2
function that rendersjson
data. Or your own implementation that accepts the two params:conn
anddata
corresponding to thePlug.Conn
and thejson
data you want to return.data
- the data we want to render asHTML
orJSON
.
Example:
quotes_controller.ex#L13
If you need more control over the rendering of HTML
or JSON
,
you can always write custom logic such as:
if Content.get_accept_header(conn) =~ "json" do
data = transform_data(q)
json(conn, data)
else
render(conn, "index.html", data: q)
end
If you want to allow people to view the JSON
representation
of any route in your application in a Web Browser
without having to manually set the Accept header
to application/json
, there's a handy function for you:
wildcard_redirect/3
To use it, simply create a
wildcard
route in your router.ex
file.
e.g:
get "/*wildcard", QuotesController, :redirect
And create the corresponding controller to handle this request:
def redirect(conn, params) do
Content.wildcard_redirect(conn, params, AppWeb.Router)
end
The 3 arguments for wildcard_redirect/3
are:
conn
- aPlug.Conn
the usual for a Phoenix controller.params
- the params for the request, again standard for a Phoenix controller.router
- the router module for your Phoenix App e.g:MyApp.Router
For an example of this in action, see:
README.md#10-view-json-in-a-web-browser
If a route does not exist in your app you will see an error. To handle this error you can use a Try Catch, e.g:
try do
Content.wildcard_redirect(conn, params, AppWeb.Router)
rescue
# below this line will only render if redirect fails:
UndefinedFunctionError ->
conn
|> Plug.Conn.send_resp(404, "not found")
|> Plug.Conn.halt()
end
Alternatively, for a more robust approach to
Error handling, see action_fallback/1
:
https://hexdocs.pm/phoenix/Phoenix.Controller.html#action_fallback/1
If you get stuck at at any point, please reference our tutorial: /dwyl/phoenix-content-negotiation-tutorial
Documentation can be found at https://hexdocs.pm/content.
If you are using this package in your project,
please ⭐ the repo on GitHub.
If you have any questions/requests,
please open an issue.