An Elixir port of Cowboy's REST sub-protocol for Plug applications.
PlugRest has two main components:
PlugRest.Router
- supplements Plug's router with aresource
macro, which matches a URL path with a Plug module for all HTTP methodsPlugRest.Resource
- defines a behaviour for Plug modules to represent web resources declaratively using multiple callbacks
PlugRest is perfect for creating well-behaved and semantically correct hypermedia web applications.
If you use Phoenix, be sure to check out PhoenixRest.
Documentation for PlugRest is available on hexdocs.
Source code is available on Github.
Package is available on hex.
Define a router to match a path with a resource handler:
defmodule MyRouter do
use PlugRest.Router
plug :match
plug :dispatch
resource "/hello", HelloResource
end
Define the resource handler and implement the optional callbacks:
defmodule HelloResource do
use PlugRest.Resource
def to_html(conn, state) do
{"Hello world", conn, state}
end
end
- Route all requests for a path to a plug, for any HTTP method
- Built-in HEAD and OPTIONS responses, and easy handling of GET, POST, PUT, PATCH, and DELETE requests
- Content negotiation for media types, languages, and charsets
- HTTP access authentication
- Observance of etag, expires, last-modified, and vary headers
- Multiple choices for redirects, not modified responses, etc.
- Correct HTTP status codes for common 400 and 500 level errors
The key abstraction of information in REST is a resource.
—Roy Fielding
In the REST architectural style, one of the uniform interface constraints is the identification of resources. Using Plug, we can satisfy this requirement by routing requests based on a URL path.
PlugRest goes a step further. Rather than manually defining HTTP semantics for each route and dividing a resource's behavior over multiple controller actions, PlugRest lets us describe our resources in a declarative way (by implementing callbacks), and follows protocol for us, including returning the correct status code when something goes wrong (or right).
PlugRest can help your Elixir application become a fluent speaker of the HTTP protocol. It can assist with content negotiation, cache headers, basic authentication, and redirects. However, it is not a full-featured framework with views, templates, sessions, or web sockets. It also lacks a requisite solution for hypermedia controls, which are essential to REST.
PlugRest is not the first REST-based framework to take a resource-oriented approach. Basho's Webmachine has inspired many such libraries, including cowboy_rest.
You can use PlugRest in a standalone web app or as part of an existing Phoenix application. Details below!
If starting a new project, generate a supervisor application:
$ mix new my_app --sup
$ cd my_app
Add PlugRest to your project in two steps:
-
Add
:plug_cowboy
and:plug_rest
to your list of dependencies inmix.exs
:defp deps do [ {:plug_cowboy, "~> 2.0"}, {:plug_rest, "~> 0.14"} ] end
Install the dependencies by running mix deps.get
and mix deps.compile
.
Create a file at lib/my_app/resources/hello_resource.ex
to hold your Resource
Handler:
defmodule MyApp.HelloResource do
use PlugRest.Resource
def allowed_methods(conn, state) do
{["HEAD", "GET", "OPTIONS"], conn, state}
end
def content_types_provided(conn, state) do
{[{"text/html", :to_html}], conn, state}
end
def to_html(conn, state) do
{"Hello #{state}", conn, state}
end
end
Create a file at lib/my_app/router.ex
to hold the Router:
defmodule MyApp.Router do
use PlugRest.Router
use Plug.ErrorHandler
plug :match
plug :dispatch
resource "/hello", MyApp.HelloResource, "World"
match "/match" do
send_resp(conn, 200, "Match")
end
end
The PlugRest Router adds a resource
macro which accepts a URL path,
a Plug module, and its options. If the module is a PlugRest.Resource
,
it will begin executing the REST callbacks, passing in any initial
state
given to it.
The router contains a plug pipeline and requires two plugs: match
and dispatch
. You can add custom plugs into this pipeline.
You can also use the match
macros from Plug.Router
.
This provides an escape hatch to bypass the REST mechanism for a
particular route and send a Plug response manually.
If no routes match, PlugRest will send a response with a 404
status
code to the client automatically.
Router paths can have segments that match URLs dynamically:
resource "/users/:id", MyApp.UserResource
The path parameters can be accessed in your resource in conn.params
:
def to_html(%{params: params} = conn, state) do
user_id = params["id"]
{"Hello #{user_id}", conn, state}
end
Finally, add the Router to your supervision tree by editing
lib/my_app/application.ex
:
children = [
{Plug.Cowboy, scheme: :http, plug: MyApp.Router, options: [port: 4001]}
]
Compile your application and then run it:
$ mix compile
$ iex -S mix
Your server will be running and the resource will be available at
http://localhost:4001/hello
.
You can generate a new PlugRest resource (with all of the callbacks implemented) by using a Mix task:
$ mix plug_rest.gen.resource UserResource
The task will create a resource at lib/my_app/resources/user_resource.ex
.
The PlugRest.Resource
module defines dozens of callbacks that offer
a declarative strategy for defining a resource's behavior. Implement
your desired callbacks and let this library do the REST, including
returning the appropriate response headers and status code.
Each callback takes two arguments:
conn
- a%Plug.Conn{}
struct; use this to fetch details about the request (see the Plug docs for more info)state
- the state of the Resource; use this to store any data that should be available to subsequent callbacks
Each callback must return a three-element tuple of the form {value, conn, state}
. All callbacks are optional, and will be given default
values if you do not define them. Some of the most common and useful
callbacks are shown below with their defaults:
allowed_methods : ["GET", "HEAD", "OPTIONS"]
content_types_accepted : none
content_types_provided : [{{"text/html"}, :to_html}]
expires : nil
forbidden : false
generate_etag : nil
is_authorized : true
last_modified : nil
malformed_request : false
moved_permanently : false
moved_temporarily : false
resource_exists : true
The docs
for PlugRest.Resource
list all of the supported REST callbacks and
their default values.
You can return representations of your resource in different formats
by implementing the content_types_provided
callback, which pairs
each content-type with a handler function:
def content_types_provided(conn, state) do
{[{"text/html", :to_html},
{"application/json", :to_json}], conn, state}
end
def to_html(conn, state) do
{"<h1>Hello</h1>", conn, state}
end
def to_json(conn, state) do
{"{\"title\": \"Hello\"}", conn, state}
end
Similarly, you can accept different media types from clients by
implementing the content_types_accepted
callback:
def content_types_accepted(conn, state) do
{[{"mixed/multipart", :from_multipart},
{"application/json", :from_json}], conn, state}
end
def from_multipart(conn, state) do
# fetch or read the request body params, update the database, etc.
{true, conn, state}
end
def from_json(conn, state) do
{true, conn, state}
end
The content handler functions you implement can return either true
,
{true, URL}
(for redirects), or false
(for errors). Don't forget
to add "POST", "PUT", and/or "PATCH" to your resource's list of
allowed_methods
.
Consult the Plug.Conn
and Plug.Parsers
docs for information on
parsing and reading the request body params.
Use Plug.Test
to help verify your resource's responses to separate
requests. Create a file at test/resources/hello_resource_test.exs
to
hold your test:
defmodule MyApp.HelloResourceTest do
use ExUnit.Case
use Plug.Test
alias MyApp.Router
test "get hello resource" do
conn = conn(:get, "/hello")
conn = Router.call(conn, [])
assert conn.status == 200
assert conn.resp_body == "Hello world"
end
end
Run the test with:
$ mix test
To help debug your app during development, add Plug.Debugger
to the
top of the router, before use Plug.ErrorHandler
:
defmodule MyApp.Router do
use PlugRest.Router
if Mix.env == :dev do
use Plug.Debugger, otp_app: :my_app
end
use Plug.ErrorHandler
# ...
end
By adding use Plug.ErrorHandler
to your router, you will ensure it
returns correct HTTP status codes when plugs raise exceptions. To set
a custom error response, add the handle_errors/2
callback to your
router:
defp handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do
send_resp(conn, conn.status, "Something went wrong")
end
You can use PlugRest's router and resources in your Phoenix app like any other plug by forwarding requests to them:
forward "/rest", HelloPhoenix.RestRouter
To get the resource
macro directly in your Phoenix router, use
PhoenixRest.
The Cowboy documentation has more details on the REST protocol:
PlugRest is still in an initial development phase. Expect breaking changes at least in each minor version.
See the CHANGELOG for more information.
PlugRest copyright © 2016, Christopher Adams
cowboy_rest copyright © 2011-2014, Loïc Hoguin essen@ninenines.eu
Plug copyright © 2013 Plataformatec.
PlugRest source code is licensed under the Apache License, Version 2.0.