Gem Version Build Status


web_pipe is a rack application builder through a pipe of operations applied to an immutable struct.

You can also think of it as a web controllers builder (the C in MVC) totally declouped from the web routing (which you can still do with something like hanami-router, http_router or plain rack's map method).

If you are familiar with rack you know that it models a two-way pipe, where each middleware in the stack has the chance to modify the request before it arrives to the actual application, and the response once it comes back from the application:

  --------------------->  request ----------------------->

  Middleware 1          Middleware 2          Application

  <--------------------- response <-----------------------

web_pipe follows a simpler but equally powerful model of a one-way pipe and abstracts it on top of rack. A struct that contains all the data from a web request is piped trough a stack of operations which take it as argument and return a new instance of it where response data can be added at any step.

  Operation 1          Operation 2          Operation 3

  --------------------- request/response ---------------->

In addition to that, any operation in the stack has the power to stop the propagation of the pipe, leaving any downstream operation unexecuted. This is mainly useful to unauthorize a request while being sure that nothing else will be done to the response.

As you may know, this is the same model used by Elixir's plug, from which web_pipe takes inspiration.

This library has been designed to work frictionless along the dry-rb ruby ecosystem and it uses some of its libraries internally.


This is a sample for a contrived application built with web_pipe. It simply fetches a user from an id request parameter. If the user is not found, it returns a not found response. If it is found, it will unauthorize when it is a non admin user or greet it otherwise:

rackup --port 4000
# http://localhost:4000?id=1 => Hello Alice
# http://localhost:4000?id=2 => Unauthorized
# http://localhost:4000?id=3 => Not found
require "web_pipe"

UsersRepo = {
  1 => { name: 'Alice', admin: true },
  2 => { name: 'Joe', admin: false }

class GreetingAdminApp
  include WebPipe

  plug :set_content_type
  plug :fetch_user
  plug :authorize
  plug :greet


  def set_content_type(conn)
      'Content-Type', 'text/html'

  def fetch_user(conn)
    user = UsersRepo[conn.params['id'].to_i]
    if user
        put(:user, user)
        set_response_body('<h1>Not foud</h1>').

  def authorize(conn)
    if conn.fetch(:user)[:admin]

  def greet(conn)
      set_response_body("<h1>Hello #{conn.fetch(:user)[:name]}</h1>")


As you see, steps required are:

  • Include WebPipe in a class.
  • Specify the stack of operations with plug.
  • Implement those operations.
  • Initialize the class to obtain resulting rack application.

WebPipe::Conn is a struct of request and response date, seasoned with methods that act on its data. These methods are designed to return a new instance of the struct each time, so they encourage immutability and make method chaining possible.

Each operation in the pipe must accept a single argument of a WebPipe::Conn instance and it must also return an instance of it. In fact, what the first operation in the pipe takes is a WebPipe::Conn::Clean subclass instance. When one of your operations calls #taint on it, a WebPipe::Conn::Dirty is returned and the pipe is halted. This one or the 'clean' instance that reaches the end of the pipe will be in command of the web response.

Operations have the chance to prepare data to be consumed by downstream operations. Data can be added to the struct through #put(key, value), while it can be consumed with #fetch(key).

Attributes and methods in WebPipe::Conn are fully documented.

Specifying operations

There are several ways you can plug operations to the pipe:

Instance methods

Operations can be just methods defined in the pipe class. This is what you saw in the previous example:

class App
  include WebPipe

  plug :hello


  def hello(conn)
    # ...

Proc (or anything responding to #call)

Operations can also be defined inline, through the with: keyword, as anything that responds to #call, like a Proc:

class App
  include WebPipe

  plug :hello, with: ->(conn) { conn }


When with: is a String or a Symbol, it can be used as the key to resolve an operation from a container. A container is just anything responding to #[].

The container to be used is configured when you include WebPipe:

class App
  Container = Hash[
    'plugs.hello' => ->(conn) { conn }

  include WebPipe.(container: Container)

  plug :hello, with: 'plugs.hello'

Operations injection

Operations can be injected when the application is initialized, overriding those configured through plug:

class App
  include WebPipe

  plug :hello, with: ->(conn) { conn.set_response_body('Hello') }

  hello: ->(conn) { conn.set_response_body('Injected') }

In the previous example, resulting response body would be Injected.

Rack middlewares

Rack middlewares can be added to the generated application through use. They will be executed in declaration order before the pipe of plugs:

class App
  include WebPipe

  use Middleware1
  use Middleware2, option_1: value_1

  plug :hello, with: ->(conn) { conn }

Standalone usage

If you prefer, you can use the application builder without the DSL. For that, you just have to initialize a WebPipe::App with an array of all the operations to be performed:

require 'web_pipe/app`

op_1 = ->(conn) { conn.set_status(200) }
op_2 = ->(conn) { conn.set_response_body('Hello') }[op_1, op_2])

Current status

web_pipe is in active development. The very basic features to build a rack application are all available. However, very necessary conveniences to build a production application, for example a session mechanism, are still missing.


Bug reports and pull requests are welcome on GitHub at

Release Policy

web_pipe follows the principles of semantic versioning.