PlugRest

Build Status Hex Version

A port of Cowboy’s cowboy_rest module to Plug.

PlugRest supplements Plug.Router with an additional resource macro, which matches a URL path with a resource handler module implementing REST semantics via a series of optional callbacks.

PlugRest is perfect for creating well-behaved and semantically correct hypermedia web applications.

Documentation for PlugRest is available on hexdocs.
Source code is available on Github.
Package is available on hex.

Hello World

Define a router to match a path with a resource handler:

defmodule MyRouter do
  use PlugRest.Router

  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

Why PlugRest?

The key abstraction of information in REST is a resource.
—Roy Fielding

Plug forms the foundation of most web apps we write in Elixir, by letting us specify a pipeline of composable modules that transform HTTP requests and responses to define our application’s behavior.

Out of the box, the original Plug Router gives us a DSL in the form of macros which generate routes:

get "/hello" do
    send_resp(conn, 200, "world")
end

The router is a plug which can match on an HTTP verb and a URL path, and dispatches the request to a function body operating on the connection.

A Phoenix router works similarly: it generates routes that match verbs and paths, which dispatch to a controller “action”.

get "/hello", HelloController, :show

Phoenix has a few extra features that help us craft an API, such as the resources macro that generates eight different verb and path pairs, and an accepts plug that assists with content negotiation.

In either case, we are given a single function in which to formulate a semantically correct HTTP response, including explicitly returning an HTTP status code if something goes wrong (or right).

For example, if a resource does not exist, we have to implement a 404 Not Found response in every single action pertaining to that resource.

Some types of responses are nearly closed off. For example, what happens when we POST a request to the above routes? Plug Router crashes and sends a 500 Internal Server Error. Phoenix shrugs and says 404 Not Found. The correct reply, of course, is 405 Method Not Allowed along with a list of supported methods in the header.

In the final analysis, Plug and Phoenix help us route requests, but it’s not enough for a hypermedia API. In order words, the rest is up to us. :smirk:

Instead of having to redefine HTTP semantics for every web application and route, we prefer to describe our resources in a declarative way, and let a library encapsulate all of the decisions, while providing sane defaults when the resource’s behavior is undefined.

Let’s see how PlugRest handles the above scenario. First we tell the router about our resource:

resource "/hello", HelloResource

And that’s it! By default our resource supports “HEAD”, “GET”, and “OPTIONS” methods. If we want to support POST, we implement an allowed_methods/2 function in our resource:

def allowed_methods(conn, state) do
  {["HEAD", "GET", "OPTIONS", "POST"], conn, state}
end

The docs for PlugRest.Resource list all of the supported REST callbacks and their default values.

Finally, it deserves mention that PlugRest is not the only library to take a resource-oriented approach to REST-based frameworks. Basho’s Webmachine has been a stable standby for years, and inspired similar frameworks in many other programming languages. PlugRest’s most immediate antecedent is the cowboy_rest module in the Cowboy webserver that currently underpins Phoenix and every other Plug-based Elixir app.

You can use PlugRest in a standalone web app or as part of an existing Phoenix application. Details below!

Installation

If starting a new project, generate a supervisor application:

$ mix new my_app --sup

Add PlugRest to your project in two steps:

  1. Add :cowboy, :plug, and :plug_rest to your list of dependencies in mix.exs:

        def deps do
          [{:cowboy, "~> 1.0.0"},
           {:plug, "~> 1.0"},
           {:plug_rest, "~> 0.5.0"}]
        end
        ```
  2. Add these dependencies to your applications list:

          def application do
            [applications: [:cowboy, :plug, :plug_rest]]
        end
        ```

Resources

Create a file at lib/my_app/hello_resource.ex to hold your Resource Handler:

defmodule MyApp.HelloResource do
  use PlugRest.Resource

  def allowed_methods(conn, state) do
    {["GET"], conn, state}
  end

  def content_types_provided(conn, state) do
    {[{"text/html", :to_html}], conn, state}
  end

  def to_html(conn, state) do
    {"Hello world", conn, state}
  end
end

Router

Create a file at lib/my_app/router.ex to hold the Router:

defmodule MyApp.Router do
  use PlugRest.Router

  resource "/hello", MyApp.HelloResource

  match "/match" do
    send_resp(conn, 200, "Match")
  end
end

The PlugRest Router adds a resource macro which accepts a URL path and a Module that will handle all the callbacks on the Resource.

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.

Dynamic path segments

Router paths can have segments that match URLs dynamically:

  resource "/users/:id", MyApp.UserResource

The path parameters can be accessed in your resource with read_path_params/1:

    def to_html(conn, state) do
      params = read_path_params(conn)
      user_id = params["id"]
      {"Hello #{user_id}", conn, state}
    end

Application

Finally, add the Router to your supervision tree by editing lib/my_app.ex:

    # Define workers and child supervisors to be supervised
    children = [
      Plug.Adapters.Cowboy.child_spec(:http, MyApp.Router, [], [port: 4001])
    ]

Running

Compile your application and then run it:

$ iex -S mix

Your server will be running and the resource will be available at http://localhost:4001/hello.

Testing

Use Plug.Test to help verify your resources’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

Phoenix

You can use PlugRest in your Phoenix app. Add :plug_rest to your dependencies, save your REST router at web/rest_router.ex, and put your resources in web/resources/. Then use the forward macro in your Phoenix web/router.ex:

  forward "/rest", HelloPhoenix.RestRouter

The resource will be served at http://localhost:4001/rest/hello.

Information

The Cowboy documentation has more details on the REST protocol:

Differences between PlugRest and cowboy_rest:

  • Each callback accepts a Plug conn struct instead of a Cowboy Req record.
  • The init/2 callback is not required. However, if it does exist, it should return {:ok, conn, state}.
  • The content callbacks (like to_html) return {body, conn, state} where the body is one of iodata or {:chunked, Enum.t}.
  • The content types provided and accepted callbacks can describe each media type with a String like "text/html"; or a tuple in the form {type, subtype, params}, where params can be %{} (no params acceptable), :* (all params acceptable), or a map of acceptable params %{"level" => "1"}.

Upgrading

PlugRest is still in an initial development phase. Expect breaking changes at least in each minor version.

See the CHANGELOG for more information.

License

PlugRest copyright © 2016, Christopher Adams

cowboy_rest copyright © 2011-2014, Loïc Hoguin essen@ninenines.eu

Cowboy logo copyright © 2016, dikaio