Chasten - Parfay Conceptual Introduction

This is a conceptual introduction to parfay and uses a lot of words on explanations. The alternative is the much terser API reference, which focuses on parfay's public contract and might be more useful after you've learned the concepts.

Parfay is the fundamental idea that the typical HTTP request -> response interaction is readily and intuitively modelled with functional programming: you write a function that takes a request as argument and returns a response, and there you are. The web server (Parfay + Node.js) will handle the business of actually sending the response over the network. All you should be concerned with is expressing your intent in pure data.

As you read this introduction, you'll come to realize that parfay does almost nothing. It is mostly a pledge you undertake to follow certain conventions and abstain from functional impurity, and in so doing, parfay and its fellow libraries have surprisingly little work to do, and you and your application code - in turn - will benefit in various ways.

Let's introduce some terminology.

Handlers

A handler is precisely what was alluded to above, a function from (Request) -> Response. Here is an example that responds, in JSON, with all the details of the request it was given:

function echoHandler(request) {
  return {
    'body': JSON.stringify(request),
    'headers': {
      'Content-Type': 'application/json',
    },
    'status': 200,
  };
}

A handler returns an HTTP status and optionally headers and a body. What to do with the information is left to Parfay.

Middlewares

Often some logic must be applied to several different handlers, and while duplicate code in several handlers might do in the small, you likely want to create reusable middlewares for these chunks of logic for projects beyond a certain size. A middleware in parfay is analogous to its counterparts in Express, Koa, et al, but the contract is different.

A middleware takes a handler and returns a (probably different) handler. This means you can wrap a handler in middleware to produce a new handler, which can be further wrapped in middleware. Each layer nests the previous stack of handler-and-middlewares inside a new middleware. If this seems wildly academic and confusing, perhaps a few examples will make the idea more tangible.

This middleware turns any response into a 204 No Content and removes the body.

function noContent(handler) {
  return async(request) => {
    const response = await handler(request);
    return {
      ...response,
      'status': 204,
      'body': null,
    };
  }
}

Notice how it calls upon the handler it was given to let the chain of middlewares and finally the handler run their code.

This middleware enriches the request object with a user property which is then available to the handler and any middlewares running after this one.

function withUser(handler) {
  return async(request) => {
    const userRequest = {
      ...request,
      'user':{
        'name': 'Azathoth',
      },
    };
    return handler(userRequest);
  }
}

The two middlewares above operate at two different points in time in relation to the request -> response cycle. The first waits for the handler to return a response, and then changes that response before returning it. The second changes the request before passing it on the handler, and then just returns whatever resulted from the handler.

These two approaches are not mutually exclusive; this next middleware adds some runtime data to both the request and the response.

Please note: the code below is impure. There are ways to make it pure, and you can read about those in the section about prithee.

function runtime(handler) {
  return async(request) => {
    const start = new Date();
    const timedRequest = {
      ...request,
      start,
    };
    const response = await handler(timedRequest);
    const end = new Date();
    const timedResponse = {
      ...response,
      start,
      end,
      'duration': end - start,
    };
    return timedResponse;
  }
}

In summary, a middleware takes a handler as input and returns a new handler. This new handler will most likely call upon the origin handler to turn the request into a response by unwinding the remainder of the middleware stack and the handler itself. Alternatively it can return its own response without calling the original handler to short-circuit the middleware stack. This is particularly useful when a request must be rejected, e.g. due to failed authentication. In this way, middlewares have the power to change both the request and response.

Here's a graphical illustration of a handler and several middlewares.

The request comes in
   |
   |        The response goes out.
   ↓        ↑
+--------------+ Changing the request here will affect middleware 2
| Middleware 1 | and down. Changing the response will affect no
+--------------+ other middlewares or handlers.
   ↓        ↑
+--------------+ Changing the request here will affect midddleware 3
| Middleware 2 | and down. Changing the response will affect
+--------------+ only middleware 1.
   ↓        ↑
+--------------+ Changing the request here will affect only the
| Middleware 3 | handler. Changing the response will affect
+--------------+ middleware 2 and up.
   ↓        ↑
+--------------+ The request becomes a response and travels back up
|   Handler    | in reverse order through the middlewares.
+--------------+

Requests

A request is an object with the following keys.

Typically a request will be enriched with additional information by middlewares before reaching the handler.

Example

{
  "body": "",
  "headers": {
    "host": "localhost:3000",
    "user-agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0",
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "accept-language": "en-US,en;q=0.5",
    "accept-encoding": "gzip, deflate",
    "dnt": "1",
    "connection": "keep-alive",
    "upgrade-insecure-requests": "1"
  },
  "httpVersion": "1.1",
  "localPort": 3000,
  "localAddress": "::ffff:127.0.0.1",
  "method": "GET",
  "remoteAddress": "::ffff:127.0.0.1",
  "url": "/pets?name=doge"
}

Responses

A response is an object with the following keys.

It might include any number of additional keys if necessary for the middlewares to work. These will not affect the HTTP response itself as Parfay only understand the concepts of a body, a status, and headers.

Example

{
  "body": "[2, 3, 5, 7, 11]",
  "headers": {
    "content-type": "application/json",
    "cache-control": "max-age=3600"
  },
  "status": 200
}