response-state.coffee

CoffeeScript version of the ResponseState pattern

Usage no npm install needed!

<script type="module">
  import responseStateCoffee from 'https://cdn.skypack.dev/response-state.coffee';
</script>

README

ResponseState.JS

Circle CI Dependency Status devDependency Status

JavaScript version of the ResponseState pattern.

The ResponseState pattern improves structure and control flow of business logic by providing

  • a clean representation of the various outcomes that complex operations can yield, and
  • an efficient control flow mechanism to react to those outcomes.

These conventions allow for a clean separation of the different layers within an application stack, through fully domain-specific APIs, while avoiding the need to abuse errors or exceptions for fine-grained, algorithmic control flow.

Using the ResponseState pattern

Lets assume our architecture has a service layer that contains a transferFunds service. This service returns its result as response states. Here is how readable code that uses it is. This code could for example live in a controller for a REST API. (Examples are in CoffeeScript for readability)

services.transferFunds from: 'checking', to: 'savings', amount: 100, (result) ->
  result.is
    success: -> send status: 201, message: 'transfer complete'
    pending: -> send status: 202, message: 'transfer pending user approval'
    limit_exceeded: -> send status: 412, message: 'daily transaction limit exceeded'
    insufficient_funds: -> send status: 403, message: 'not enough funds'
    unknown_account: -> send status: 404, message: 'unknown account given'
    unauthorized: -> send status: 401, message: 'please log in first'
    other: -> send status: 500, message: 'please try again later'

In this example, the result is an instance of the Response class provided by this library. It has an is method, which calls the matching handler from the given hash of response state handlers. This represents the different code paths that deal with the different outcomes of the attempt to create a transfer in a very readable and maintainable way.

The special other response state acts as a catch-all, i.e. it is called if none of the states listed match the current condition. If no such catch-all handler is provided, unmatched errors cause an exception in the is method.

Implementing ResponseState APIs

With the help of this library, the API of the transferFunds service above can be implemented super easy like this:

Response = require 'response-state'

services.transferFunds = ({from, to, amount}, done) ->
  if not (fromAccount = getAccount from) then return done new Response('unknown_account')
  if from.balance < amount then return done new Response('insufficient_funds')
  if from.limit < amount then return done new Response('limit_exceeded')
  ...

  done null, new Response('transfer_finished', {confirmation, url})

Data handlers

The is method returns the result of the respective response state handler function. If the handler is not a function, it is returned directly. This allows to refactor the example above into:

services.transferFunds from: 'checking', to: 'savings', amount: 100, (result) ->
  [status, message] = result.is
    success: [201, 'transfer complete']
    pending: [202, 'transfer pending user approval']
    limit_exceeded: [412, 'daily transaction limit exceeded']
    insufficient_funds: [403, 'not enough funds']
    unknown_account: [404, 'unknown account given']
    unauthorized: [401, 'please log in first']
    other: [500, 'please try again later']
  send {status, message}

When to use

Response states are an optional pattern that should be used casually, i.e. only if the value it adds exceeds the complexity it introduces. This is probably mostly the case across larger functional boundaries, when calling into different architectural layers that perform complex operations. The responsibility of verifying the overall correctness of the code's behavior, i.e. that it transfers the right amounts between the right accounts at the right times, for the right reasons, remains with your test suite.

Development

See the developer documentation