Permalink
3df2210 Nov 5, 2018
2 contributors

Users who have contributed to this file

@flintinatux @pklingem
430 lines (307 sloc) 14.6 KB

API

Function Description
cors CORS support wrapper
html response helper, type text/html
json response helper, type application/json
logger json request logger
methods maps request methods to handler functions
mount top-level server function wrapper
parseJson json body parser
redirect redirect response helper
routes maps express-style route patterns to handler functions
send basic response helper
serve static file serving handler

cors

cors :: ((Request -> Promise Response), { k: v }) -> Request -> Promise Response

Wraps a top-level handler function to add support for CORS. Lifts the handler into a Promise chain, so the handler can respond with either a Response, or a Promise that resolves with one. Also accepts an object with the following optional properties to override the default CORS behavior.

Property Type Overridden header Default
credentials String access-control-allow-credentials 'true'
headers String access-control-allow-headers 'content-type'
methods String access-control-allow-methods 'GET,POST,OPTIONS,PUT,PATCH,DELETE'
origin Any access-control-allow-origin '*'

Acceptable values for origin are:

Value Type Description
'*' String wildcard, allows any origin access, but only without credentials
true Boolean reflect the origin of the request
/domain.com/ Regex validate the origin against regex

See also parseJson, serve.

const { always } = require('ramda')
const http = require('http')
const { cors, mount, send } = require('paperplane')

const endpoint = req =>
  Promise.resolve(req.body).then(send)

const opts = {
  headers: 'x-custom-header',
  methods: 'GET,PUT'
}

const app = cors(endpoint, opts)

http.createServer(mount({ app })).listen(3000)

html

html :: (String | Buffer | Stream) -> Response

Returns a Response, with the content-type header set to text/html.

See also json, redirect, send.

const { html } = require('paperplane')
const template = require('../views/template.pug')

const usersPage = () =>
  fetchUsers()
    .then(template)
    .then(html)

In the example above, it resolves with a Response similar to:

{
  body: '<html>...</html>',
  headers: {
    'content-type': 'text/html'
  },
  statusCode: 200
}

json

json :: a -> Response

Returns a Response, with a body encoded with JSON.stringify, and the content-type header set to application/json.

See also html, redirect, send.

const { json } = require('paperplane')

const users = () =>
  fetchUsers()
    .then(json)

In the example above, it resolves with a Response similar to:

{
  body: '[{"id":1,"name":"Scott"}]',
  headers: {
    'content-type': 'application/json'
  },
  statusCode: 200
}

logger

logger :: a -> a

Logs request/response as json to console.info. Uses the following whitelist:

{
  req: ['headers', 'method', 'url'],
  res: ['statusCode']
}

If the logged value is an error, it only logs ['message', 'name', 'stack'].

Note: This is the default logger used by mount, but you can also require it to modify as needed. For example, in dev mode you may prefer not to log headers, like this:

const { compose, dissocPath } = require('ramda')
const http = require('http')
const { logger: log, mount, send } = require('paperplane')

const app = () =>
  send() // 200 OK

const isProd = process.env.NODE_ENV === 'production'

const logger = isProd ? log : compose(log, dissocPath(['req', 'headers']))

http.createServer(mount({ app, logger })).listen(3000)

Logs will be formatted as json, similar to below:

{"req":{"headers":{"host":"localhost:3000","connection":"keep-alive","cache-control":"no-cache","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36","content-type":"application/json","accept":"*/*","accept-encoding":"gzip, deflate, sdch, br","accept-language":"en-US,en;q=0.8"},"method":"GET","url":"/courses/"},"res":{"statusCode":200}}

methods

methods :: { k: (Request -> Promise Response) } -> (Request -> Promise Response)

Maps handler functions to request methods. Returns a handler function. If the method of an incoming request doesn't match, it rejects with a 404 Not Found. Use in combination with routes to build a routing table of any complexity.

Note: If you supply a GET handler, paperplane will also use it to handle HEAD requests.

See also mount, routes.

const http = require('http')
const { methods, mount } = require('paperplane')

const { createUser, fetchUsers } = require('./api/users')

const app = methods({
  GET:  fetchUsers,
  POST: createUser
})

http.createServer(mount({ app })).listen(3000)

mount

mount :: { k: v } -> (IncomingMessage, ServerResponse) -> ()

Wraps a top-level handler function to prepare for mounting as a new http server. Lifts the handler into a Promise chain, so the handler can respond with either a synchronous Response, or a Promise or other ADT that resolves with one. Accepts the following options:

Option Type Default Description
app Request -> m Response R.identity top-level request handler function
cry Error -> a paperplane.logger error logger
lambda Boolean false enables support for serverless deployment
logger a -> a paperplane.logger request/response logger
middleware [ ReduxMiddleware ] [] list of Redux middleware for unwrapping ADT's

The cry option is primarily intended for logging, but is also the correct way to notify your error aggregation service. All errors passed to the cry function will have a req property that can be used to include request information in your notification. For notifying the following error aggregation services, please use the latest version of the library listed:

Service Library
Airbrake paperplane-airbrake
Bugsnag paperplane-bugsnag

To support request handlers that return ADT's (such as those provided by the lovely crocks library), register a list of appropriate Redux middleware using the middleware option. No, paperplane does not use Redux, but Redux middleware make for great little plugins to recursively unwrap ADT's. You won't need redux-promise, because Promise support is already included, but some common middlewares that may interest you are:

See also methods, routes.

const { Async, IO } = require('crocks')
const { compose, pipe, pipeP } = require('ramda')
const future = require('redux-future2')
const http = require('http')
const io = require('redux-io').default
const { json, mount, parseJson, routes } = require('paperplane')

const airbrake = require('./lib/airbrake')
const logger   = require('./lib/logger')

const endpoints = routes({
  '/async':     Async.of,
  '/io':        IO.of,
  '/promise':   Promise.resolve,
  '/inception': compose(Async.of, IO.of)
})

const app = pipe(
  parseJson,
  pipeP(
    endpoints,
    json
  )
)

const cry = require('paperplane-airbrake')(airbrake)

const middleware = [ future, io('run') ]

http.createServer(mount({ app, cry, logger, middleware })).listen(3000, cry)

Serverless deployment

If { lambda: true } is enabled, the mount function returns a Lambda handler suitable for use as the backend of an API Gateway Lambda proxy integration. A special context property will be added to the Request to provide access to the requestContext from the Lambda proxy event, which provides access to identity information supplied by Lambda authorizers or Cognito user pools. Otherwise, building your app with paperplane is business as usual, just with the scaling power of a serverless deployment.

const { always } = require('ramda')
const { json, methods, mount, routes } = require('paperplane')

const courses = require('./data/courses')

const app = routes({
  '/api/courses': methods({
    GET:  courses.list,
    POST: courses.create,
    PUT:  courses.update
  }),

  '/health': always(send()) // 200 OK
})

exports.handler = mount({ app, lambda: true })

parseJson

parseJson :: Request -> Request

Parses the request body as json if available, and if the content-type is application/json. Otherwise, passes the Request through untouched.

See also cors, serve.

const { compose } = require('ramda')
const http = require('http')
const { mount, parseJson, json } = require('paperplane')

const echo = req =>
  Promise.resolve(req.body).then(json)

const app = compose(echo, parseJson)

http.createServer(mount({ app })).listen(3000)

redirect

redirect :: (String, Number) -> Response

Accept a Location and optional statusCode (defaults to 302), and returns a Response denoting a redirect.

Pro-tip: if you want an earlier function in your composed application to respond with a redirect and skip everything else, just wrap it in a Promise.reject (see example below). The error-handling code in paperplane will ignore it since it's not a real error.

See also html, json, send.

const { compose, composeP } = require('ramda')
const http = require('http')
const { html, methods, mount, parseJson, routes, send } = require('paperplane')

const login = require('./views/login')

// Please make your authorization better than this
const authorize = req =>
  req.headers.authorization
    ? Promise.resolve(req)
    : Promise.reject(redirect('/login'))

const echo = req =>
  Promise.resolve(req.body).then(send)

const app = routes({
  '/echo': methods({
    POST: composeP(echo, authorize)
  }),

  '/login': methods({
    GET: compose(html, loginPage)
  })
})

http.createServer(mount({ app })).listen(3000)

In the example above, redirect() returns a Response similar to:

{
  body: '',
  headers: {
    Location: '/login'
  },
  statusCode: 302
}

routes

routes :: { k: (Request -> Promise Response) } -> (Request -> Response)

Maps handler functions to express-style route patterns. Returns a handler function. If the path of an incoming request doesn't match, it rejects with a 404 Not Found. Use in combination with methods to build a routing table of any complexity.

See also methods, mount.

const http = require('http')
const { mount, routes } = require('paperplane')

const { fetchUser, fetchUsers, updateUser } = require('./lib/users')

const app = routes({
  '/users': methods({
    GET: fetchUsers
  }),

  '/users/:id': methods({
    GET: fetchUser,
    PUT: updateUser
  })
})

http.createServer(mount({ app })).listen(3000)

send

send :: (String | Buffer | Stream) -> Response

The most basic response helper. Simply accepts a body, and returns a properly formatted Response, without making any further assumptions.

See also html, json, redirect.

const { send } = require('paperplane')

send('This is the response body')

In the example above, it returns a Response similar to:

{
  body: 'This is the response body',
  headers: {},
  statusCode: 200
}

serve

serve :: { k: v } -> (Request -> Response)

Accepts an options object (see details here), and returns a handler function for serving static files. Expects a req.params.path to be present on the Request, so you'll need to format your route pattern similar to the example below, making sure to include a /:path+ route segment.

Note: this was previously static, but that's a keyword, so I renamed it.

See also cors, parseJson.

const http = require('http')
const { mount, routes, serve } = require('paperplane')

const app = routes({
  '/public/:path+': serve({ root: 'public' })
})

http.createServer(mount({ app })).listen(3000)