passing-notes

Build an HTTP server out of composable blocks

Usage no npm install needed!

<script type="module">
  import passingNotes from 'https://cdn.skypack.dev/passing-notes';
</script>

README

passing-notes

npm CI Status dependencies Status devDependencies Status

Build an HTTP server out of composable blocks

Example

// server.mjs
export default function(request) {
  console.log(request)

  return {
    status: 200,
    headers: {
      'Content-Type': 'text/plain'
    },
    body: 'Hello World!'
  }
}
yarn pass-notes server.mjs

Handle HTTP requests with a function that takes request data and returns response data. Compose your functions with pre-built functions to quickly implement complex workflows.

Installation

Install passing-notes by running

yarn add passing-notes

Concepts

passing-notes provides an interface for building HTTP servers. At its core, it takes a function that takes in request data and returns response data.

// server.mjs

export default function(request) {
// request = {
//  version: '2.0',
//   method: 'GET',
//   url: '/',
//   headers: {},
//   body: ''
// }

  return {
    status: 200,
    headers: {
      'content-type': 'text/plain'
    },
    body: 'Hello World!'
  }
}

This code can be run either from the command line:

yarn pass-notes server.mjs

Or from JavaScript:

import {startServer} from 'passing-notes'
import handleRequest from './server.mjs'

startServer({port: 8080}, handleRequest)

Middleware

Taking cues from popular tools like Express, we encourage organizing your request-handling logic into middleware:

import {compose} from 'passing-notes'

export default compose(
  (next) => (request) => {
    const response = next(request)
    return {
      ...response,
      headers: {
        ...response.headers,
        'content-type': 'application/json'
      },
      body: JSON.stringify(response.body)
    }
  },
  (next) => (request) => {
    return {
      status: 200,
      headers: {},
      body: {
        message: 'A serializable object'
      }
    }
  }
)

Each request is passed from top to bottom until one of the middleware returns a response. That response then moves up and is ultimately sent to the client. In this way, each middleware is given a chance to process and modify the request and response data.

Note that one of the middleware must return a response, otherwise, an Error is thrown and translated into a 500 response.

Pre-Built Middleware

We've built and packaged some middleware that handle common use cases:

  • static: Serves static files from the file system
  • ui: Serves application code to the browser
  • rpc: Simple communication between browser and server

Developer Affordances

When using the pass-notes CLI tool, during development (when NODE_ENV !== 'production'), additional features are provided:

Hot Reloading

The provided module and its dependencies are watched for changes and re-imported before each request. Changes to your code automatically take effect without you needing to restart the process.

The node_modules directory, however, is not monitored due to its size.

Self-Signed Certificate

HTTPS is automatically supported for localhost with a self-signed certificate. This is needed for browsers to use HTTP/2.0 when making requests to the server.

Per-Environment Configuration

A common pattern for implementing per-environment configuration is to store that configuration in a file that is modified per environment. This is useful for scenarios for which it's not convenient to directly set environment variables.

We support setting environment variables via a .env.yml file:

FOO: string
BAR:
  - JSON
  - array
BAZ:
  key1: JSON
  key2: object

Logging

By default, the method and URL of each request and the status of the response is logged to STDOUT, alongside a timestamp and how long it took to return the response.

To log additional information:

import {Logger} from 'passing-notes'

export const logger = new Logger()

export default function(request) {
  logger.log({
    level: 'INFO',
    topic: 'App',
    message: 'A user did a thing'
  })

  // ...
}

In addition, our Logger provides a way to log the runtime for expensive tasks, like database queries:

const finish = logger.measure({
  level: 'INFO',
  topic: 'DB',
  message: 'Starting DB Query'
})

// Perform DB Query

finish({
  message: 'Finished'
})

The logger can be passed to any middleware that needs it as an argument.

API

pass-notes server.js

A CLI that takes an ES module that exports an HTTP request handler and uses it to start an HTTP server.

// server.mjs
export default function(request) {
  console.log(request)

  return {
    status: 200,
    headers: {
      'Content-Type': 'text/plain'
    },
    body: 'Hello World!'
  }
}
yarn pass-notes server.mjs

Request Handler

The ES module's default export must be a function that takes as argument an object with the following keys:

  • version: The HTTP version used, either '1.1' or '2.0'
  • method: An HTTP request method in capital letters (e.g. GET or POST)
  • url: The absolute URL or path to a resource
  • headers: An object mapping case-insensitive HTTP header names to values
  • body: The HTTP request body as a string or buffer

And returns an object (or Promise resolving to an object) with the following keys:

  • status: The HTTP response status code (e.g. 200)
  • headers
  • body
  • push: An optional array of requests that will be fed back into the request handler to compute responses and then pushed to the client. This is only supported over HTTP/2 (indicated by request.version being '2.0').

Protocol Support

This HTTP server supports HTTP/1.1 and HTTP/2 as well as TLS.

TLS Configuration

A self-signed certificate is automatically generated for localhost when NODE_ENV is not set to production. Otherwise, a certificate can be provided by exporting an object named tls containing any of the options for tls.createSecureContext, for example:

export const tls = {
  cert: 'PEM format string',
  key: 'PEM format string'
}

CERT and KEY can also be provided as environment variables.

Hot-Reloading

When NODE_ENV is not set to production, the provided ES module is re-imported whenever it or its dependencies change. Note that node_modules are never re-imported.

Logging

By default, the method and URL for every request is logged to STDOUT.

In order to log additional events to STDOUT, a custom logger can be created and exported:

import {Logger} from 'passing-notes'

export const logger = new Logger()

This logger is expected to provide the following interface:

  • It extends EventEmitter
  • It emits log events with two arguments:
    • event: An object containing:
      • time: A UNIX timestamp
      • level: One of TRACE, DEBUG, INFO, WARN, ERROR, or FATAL
      • topic: A string that categorizes the log event
      • message: A description of the log event
      • duration: An optional millisecond duration
      • error: An optional Error object to print
    • logLine a formatted string to print to STDOUT
  • log(event): Computes a timestamp and emits a log event.
  • measure(event): Logs the start of a task. Returns a function that when called, computes the duration and logs the end of the task.