- Notifications
You must be signed in to change notification settings - Fork1
🔌 A Plug to add Content Negotiation to any Phoenix App so you can render HTML or JSON for the same route.
License
dwyl/content
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
content
adds Content Negotiationtoany Phoenix Appso you can render HTML and JSON for thesame route.
We need toreduceeliminate duplication of effortwhile building our App+API so we can ship featuresmuch faster.
Using this Plug we are able to build our App (Phoenix Web UI)and a REST (JSON) API in thesame codebase withminimal effort.
A Plug that can be added toany Phoenix Appto render bothHTML
andJSON
in thesame route/controllerso that we save dev time.By ensuring that all Web UIhas a corresponding JSON responsewe guarantee thateveryone hasaccess to their data in the most convenient way.
Returning anHTML
view for people using the App in a Web Browserand returningJSON
for people requesting thesame endpointfrom a script (or a totally independent front-end)we guarantee that all features of our Web Appare automatically available in the API.
We have built several Apps and APIs in the pastand felt the pain of having to maintaintwo separate codebases.It's fine formega corpwith hundreds/thousandsof developers to maintain aseparate web UIand API applications.We are a small teamthat has to do (a lot) more with fewer resources!
If you are new to content negotiation ingeneralorhow to implement it in Phoenix from scratch,please see:dwyl/phoenix-content-negotiation-tutorial
This project is "for us by us".We areusing it in our product in production.It serves our needsexactly.As witheverything we do it's Open Sourceso that anyone else can benefit.If it looks useful to you, use it!If you have any ideas/requests for features,please open anissue.
Inless than2 minutes and 3 easy stepsyou will have content negotiation enabledin your Phoenix Appand can get back to building your app!
Addcontent
to your list of dependencies inmix.exs
:
defdepsdo[{:content,"~> 1.3.0"}]end
Then runmix deps.get
.
Open therouter.ex
file in your Phoenix App.Locate thepipeline :browser do
section.And replace it:
Before:
pipeline:browserdoplug:accepts,["html"]plug:fetch_sessionplug:fetch_flashplug:protect_from_forgeryplug:put_secure_browser_headersend
After:
pipeline:anydoplug:accepts,["html","json"]plugContent,%{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"/",AppWebdopipe_through(:browser)get("/",PageController,:index)end
After:
scope"/",AppWebdopipe_through(:any)get("/",PageController,:index)end
Pass the plugs you want to run forhtml
ashtml_plugs
(in the order you want to execute them).
Note: the
&
and/2
additions to the names of plugsare theElixir
way of passing functions by reference.
The&
means "capture" and the/2
is thearityof the function we are passing.
We wouldobviously prefer if functions were just variableslike they are in some other programming languages,but thisworks.
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 invokeContent.reply/5
which will renderHTML
orJSON
depending on theaccept
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.
TheContent.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 thattakesconn
,template
anddata
as it's 3 params.template
- the.html
template to be renderedif theaccept
header matches"html"
; e.g:"index.html"
json/2
- thePhoenix.Controller.json/2
functionthat 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 ofHTML
orJSON
,you can always write custom logic such as:
ifContent.get_accept_header(conn)=~"json"dodata=transform_data(q)json(conn,data)elserender(conn,"index.html",data:q)end
If you want to allow people to view theJSON
representationofany route in your application in a Web Browserwithout having tomanually set the Accept headertoapplication/json
, there's a handy function for you:wildcard_redirect/3
To use it, simply create awildcardroute in yourrouter.ex
file.e.g:
get"/*wildcard",QuotesController,:redirect
And create the corresponding controller to handle this request:
defredirect(conn,params)doContent.wildcard_redirect(conn,params,AppWeb.Router)end
The 3 arguments forwildcard_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 aTry Catch,e.g:
trydoContent.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 toError handling, seeaction_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 athttps://hexdocs.pm/content.
If you areusing this package in your project,please ⭐ the repo on GitHub.
If you have any questions/requests,please open anissue.
About
🔌 A Plug to add Content Negotiation to any Phoenix App so you can render HTML or JSON for the same route.