Skip to content
Breadcrumb
Resources

Coach, a few years on

Lisa Karlin Curtis
Written by

Last editedOct 2020

Here at GoCardless we’ve been using Coach to power our routes for about 5 years now, and we thought it could be interesting to share a few things we’ve learned.

We have just over 100 middlewares, 10 for Authentication, 15 for providing and manipulating models and collections, about 40 general utilities and the rest are used in a specific part of our API (e.g. the oauth flow).

Middlewares are great

The Coach pattern of middlewares - i.e. a bit of code shared between routes that has access to a request context - is still a strong one. You see the same in NodeJS with Express, Go http.Handler, and many others. It makes our routes easy to read, and keeps ‘route-like’ code nicely separate from the rest of the application. A simple example:

class APIVersion < Coach::Middleware
  provides :api_version

  def call
    if api_version.blank?
      return Renderer.error(Constants::Errors::MISSING_VERSION_HEADER,
                            request: request)
    end

    unless api_version.match?(Constants::Versions::FORMAT)
      return Renderer.error(
        Constants::Errors::VERSION_HEADER_BADLY_FORMATTED,
        request: request,
      )
    end

    unless api_version.in?(Constants::Versions::SUPPORTED_RELEASES)
      return Renderer.error(Constants::Errors::VERSION_NOT_FOUND,
                            request: request)
    end

    provide(api_version: api_version)
    next_middleware.call
  end

  private

  def api_version
    @api_version ||= request.headers[Constants::Versions::HEADER]
  end
end

We can also use config to make really readable statements by naming our config variables well. There are lots of interesting use cases, my favourite is how we manage Collections.

Many of our APIs return an ActiveRecord collection, and so to help us we’ve built a series of middlewares which allow you to manipulate a collection. Each middleware requires and provides the coach variable collection to ensure you can pick and choose different combinations. Some examples are:

Collection::Provide which receives a Proc to instantiate a collection. It is given as a proc so that, when loading the class, no connection to Postgres is accidentally established (which would happen if we called a scope inside the collection definition).

module Collection
   class Provide < Coach::Middleware
      provides :collection

      def call
        provide(collection: config.fetch(:collection).call)

        next_middleware.call
      end
    end
end

Filter takes a config param :using which defines a filter function.

class Filter < Coach::Middleware
  requires :collection
  provides :collection

  def call
    provide(collection: filter.new(collection, request.query_string).apply)
    next_middleware.call
  end

  def filter
    config.fetch(:using)
  end
end

GC has a multi-tenant system, so we use ByOrg to manage that in a reusable way. The ActiveRecord models implement a for_organisation_ids scope to make this work:

class ByOrg < Coach::Middleware
  requires :organisation, :collection, :gocardless_admin?
  provides :collection

  def call
    if unscoped_access?
      provide(collection: collection.all)
    else
      provide(collection: collection.for_organisation_ids(organisation.id))
    end
    next_middleware.call
  end

  private

  def unscoped_access?
    gocardless_admin? && config.fetch(:unscoped_global_access, false)
  end
end

Overall Usage:

uses Middleware::Collection::Provide, collection: -> { MyModel.active }
uses Middleware::Collection::ByOrg
uses Middleware::Collection::Filter, using: MyModelRouteFilter
uses Middleware::Collection::IncludeAssociations, map: { "parent" => :parent }

# this provides :paginated_collection and should always be called last
uses Middleware::Collection::Paginate

There is such a thing as too many Middlewares

“Middlewares are like a tool box, but if there are too many tools in the box, you’ll never find the right one”. This results in routes that are missing middlewares they probably should have, and duplicated logic across middlewares. We have been guilty of this at times, for a couple of primary reasons: As with other parts of large code projects, it’s not always easy to identify when code within a middleware becomes obsolete (e.g. a parameter or code path that is never used). The uses pattern means that it can be even harder for tools like debride to help you out with this, so keeping it well maintained requires serious dedication to the cause. It can be tempting to write one-use middleware if you want to leverage other middlewares later in the chain. (e.g. something that does something complex and specific before you run your standard Paginate middleware). I think there are a couple of potential solutions here: either have a standard way of managing one-use middlewares (maybe in their own namespace), or build tooling that allows you to pass a service object into something that dynamically creates your middleware for you (which while clever, may confuse both your colleagues and coach-cli) - see the Provide middleware at the end of this article for some inspiration.

Nested Middlewares are dangerous

Middlewares can, of course, use other middleware. This is a powerful pattern, but should be treated with caution. It is not hard to generate a middleware tree which no developer can keep in their head, and thus can cause mistakes (just like anything that’s obfuscated behind many classes / files). I think the key to this one is having clear middleware names, and not nesting middlewares more than 2, or if you really have to 3 deep. If you’re using lots of very small Middlewares (e.g. to parse particular http headers) - perhaps look at whether a Utility function might serve you better. I’d also avoid class inheritance if you don’t really need it - it’s possible to write some quite confusing code this way. Having said that, there are definitely some situations where it is the right call to nest middlewares. When you have lots of middlewares which you want to call on pretty much all your routes, you have a trade-off between being really declarative (the route specifying all the middlewares) and ultra-safe (developers can’t accidentally forget one of your [10] standard middlewares because they’re rolled into one, and you have a cop that makes sure it’s present on all routes except a specified whitelist).

class Authenticate < Coach::Middleware
  uses Middleware::Auth::GetAccessToken
  uses Middleware::Auth::RecordUserIpAddress
  uses Middleware::Auth::RestrictApiUsage
  uses Middleware::Auth::SetCookies
end
class GetAccessToken < Coach::Middleware
  uses Middleware::Auth::ParseAuthorizationHeaderBearer

  requires :api_version
  provides :access_token, :organisation, :user_id, :partner_app, :api_scope

  (...)
end

Control flow with middlewares is hard

This might seem obvious, but I've been surprised how often I've hit this specific obstacle - you can't conditionally call a middleware. It's unlike most other coding, where if blocks are a staple, so requires you to think in a particular way that may or may not come naturally. This can result in some strange patterns, including middleware that simply provide default versions of another middleware's provides to enable future middlewares to run. We sometimes get around this particular issue by pulling something out of Coach's internal @context object directly (e.g. user: @context.fetch(:user, nil)), which of course removes your contract with Coach that the context variable you require will always be present. There's not an easy solution here, but I suppose one angle is that if you really need lots of conditionally called code in your route, middlewares might not be the right pattern for you.

Another issue you may encounter is that you can’t pass config from a top level middleware into nested Middlewares. When you think about what Coach is actually doing under the hood, it is understandable; config is not designed to be dynamic, and when you use a Middleware it is going to run before its caller is invoked. That does mean as a developer it can be quite un-intuitive to use (when you’re used to calling plain old functions anyway).

You can use middlewares to run code after your route has executed

Most use cases for middlewares are top-to-bottom as the request is processed - e.g. check the headers, authenticate the request, grab and manipulate the collection. However, there are some cases where it’s useful to run code after the route has returned its response. An obvious example is logging the API response (e.g. for status code and timing graphs) - you can also use it to validate the response is of an expected shape (i.e. conforms to your schema). An example:

class TrackMetrics < Coach::Middleware
  def call
    begin
      status, = result = next_middleware.call

      METRIC.increment(labels: { status: status, use_case: use_case })
    rescue StandardError
      METRIC.increment(labels: { status: 500, use_case: use_case })
      raise
    end
    result
  end
end

You can hack Coach

A final gift. This is a controversial file at GC - some people think it’s brilliant, others want to burn it to the ground.

module Middleware
  # This object takes a map of properties and functions to invoke in the request context,
  # and builds an anonymous middleware which actually "provides" the results of these
  # blocks to downstream dependencies.
  #
  # The main objective is to be able to, within a given route, dynamically define the
  # properties that you want to be passed around within the request chain.
  #
  # @param config [Hash{Symbol => #call}] a list of properties to be passed into the chain
  #
  # @example Constructing a collection of models
  # uses Middleware::Provide.new(collection: -> { Customer.all })
  #
  # @example Plucking a key from the parameters
  # uses Middleware::Provide.new(limit: ->(req) { req.params[:limit] })
  #
  # @example Assigning multiple properties
  # uses Middleware::Provide.new(foo: ->(req) { req.params[:foo] },
  #                              bar: -> { false })
  #
  # @example Using a method reference
  # uses Middleware::Provide.new(collection: method(:base_collection))
  module Provide
    # ^ this is a module because .new returns an instance of an anonymous class
    def self.new(config = {})
      config.each_value do |func|
        raise ArgumentError, <<~MSG if func.arity.negative? || func.arity > 1
          Callable objects given to Middleware::Provide must accept either a single
          `request` argument, or no arguments at all, e.g.:
            uses Middleware::Provide.new(foo: ->(req) { req.params[:foo] })
            or
            uses Middleware::Provide.new(foo: -> { :bar })
        MSG
      end

      Class.new(Coach::Middleware) do
        provides(*config.keys)

        define_method(:call) do
          provide(config.
            transform_values { |f| f.arity.zero? ? f.call : f.call(request) })
          next_middleware.call
        end

        define_singleton_method(:name) do
          "Middleware::Provide#{config.keys.inspect}"
        end
      end
    end
  end
end

Over 85,000 businesses use GoCardless to get paid on time. Learn more about how you can improve payment processing at your business today.

Get StartedLearn More
Interested in automating the way you get paid? GoCardless can help
Interested in automating the way you get paid? GoCardless can help

Interested in automating the way you get paid? GoCardless can help

Contact sales