Do you use bare Plug for simple API projects?

Do you ever use just Plug for simple web API projects (without Phoenix or other frameworks)?

3 Likes

Almost never. I stick with Phoenix and the lovely contexts what it brings in. :slight_smile:

4 Likes

I have, at work we have it set up for a few status endpoints. Which I guess is the simplest API and it works nicely.

On my own projects I have moved away from the whole plug ecosystem. :-p

2 Likes

What are you using instead?

2 Likes

The core interface is now pretty stable but there are not many ready made middleware at this point.
On the flip side I have found writting my own middleware trivial.

3 Likes

Looks nice are there any example apps built with it?

1 Like

I talked about building a project with it last week. This talk has quite a lot of docker which you may or may not find interesting

http://crowdhailer.me/talks/2017-10-31/live-coded-chat-app-in-45-minutes/

4 Likes

If I don’t need HTML, nor channels, and just a simple API with few endpoints I use just plugs. It’s trivial to code something like this, but those are mostly trivial, throwaway projects. If I needed some magic like auto created mappings from web forms to database tables I’d go with Phoenix (or If I needed e.g. channels) and wouldn’t think about it twice. After all it’s just a frontend to your application. If you have good architecture, you can swap between Phoenix and bare plugs without any problem.

4 Likes

I usually use plug + absinthe for creating web interfaces for ios apps.

2 Likes

I think Plug.Builder is under appreciated.

Rather than controllers which group handlers around a resource, you can create a module for each route handler and express the parameters validations, user authorisation, data updates and response generation as separate function plugs.

The beauty is that you can halt the pipeline and return the appropriate response code at any step, without needing the with/else/fallback_controller style of a Phoenix controller.

You can even just use this style along with a Phoenix router and Endpoint which has nice test helpers and URL helpers.

8 Likes

When there is something where I don’t want to use graphql, I also turn what used to be phoenix controllers into plugs and group them with plug routers.

3 Likes

An example code would be highly appreciated.

3 Likes

Here’s an example from a demo application we created for learning exercises:

The router looks pretty much like a phoenix router:

  scope "/api", Scout.Web do
    pipe_through :api

    post "/surveys", SurveyController, :create
    get "/surveys/:id", SurveyShowPlug, :show, as: :survey
  end

SurveyController is a typical phoenix controller, but SurveyShowPlug is a module plug, using Conn.assigns to pass data between pipeline steps.

defmodule Scout.Web.SurveyShowPlug do
  use Plug.Builder
  import Plug.Conn, only: [put_resp_content_type: 2]

  plug :put_resp_content_type, "application/json"
  plug :validate
  plug :authorize
  plug :query
  plug :respond

  def validate(conn, _) ... validate params or halt with 422
  def authorize(conn, _) ... validate auth header or halt with 401 or 403
  def query(conn, _) ... call into business logic, or halt with 404 if no such resource
  def respond(conn, _) ... send response with 200
end
Full source
defmodule Scout.Web.SurveyShowPlug do
  use Plug.Builder
  alias Plug.Conn
  import Plug.Conn, only: [put_resp_content_type: 2]

  plug :put_resp_content_type, "application/json"
  plug :validate
  plug :authorize
  plug :query
  plug :respond

  @doc "Function plug to validate a GET /surveys/:id request"
  def validate(conn = %Conn{params: %{"id" => id}}, _opts) do
    with [] <- Scout.Util.ValidationHelpers.validate_uuid(:id, id) do
      Conn.assign conn, :survey_id, id
    else
      [id: error_msg] ->
        conn
        |> Conn.send_resp(422, Poison.encode! %{errors: %{id: [error_msg]}})
        |> Conn.halt()
    end
  end

  @doc "Function plug to authorize the current user for accessing GET /surveys/:id request"
  def authorize(conn = %Conn{assigns: %{survey_id: _id}}, _opts) do
    with ["123"] <- Conn.get_req_header(conn, "authorization") do
      Conn.assign conn, :user_id, 123
    else
      [] ->
        conn
        |> Conn.send_resp(401, Poison.encode! %{errors: %{user: ["Unauthorized"]}})
        |> Conn.halt()
      _ ->
        conn
        |> Conn.send_resp(403, Poison.encode! %{errors: %{user: ["Forbidden"]}})
        |> Conn.halt()
    end
  end

  @doc "Function plug to load a survey by ID from the database"
  def query(conn = %Conn{assigns: %{survey_id: id}}, _opts) do
    with {:ok, survey} <- Scout.Core.find_survey_by_id(id) do
      Conn.assign conn, :survey, survey
    else
      {:error, reason} ->
        conn
        |> Conn.send_resp(404, Poison.encode! %{errors: reason})
        |> Conn.halt()
    end
  end

  @doc "Function plug to serialize a Survey to JSON and respond"
  def respond(conn = %Conn{assigns: %{survey: survey}}, _opts) do
    response = %{
      name: survey.name,
      state: survey.state,
      started_at: survey.started_at,
      finished_at: survey.finished_at,
      response_count: survey.response_count
    }
    Conn.send_resp(conn, 200, Poison.encode!(response))
  end
end
4 Likes

Thank you for sharing. There are situations when there is a need to define
routes outside normal controllers . This approach makes that possible.