npm package: @phylum/core
Description: PhylumJS Web Server Core
Author: Max J. Polster
Latest version: 0.1.3

# [![PhylumJS](media/header.png)](https://github.com/phylumjs)

# Concepts
PhylumJS Core is a web server library that provides performant scalable routing and a thin api layer on top of node's http modules. PhylumJS is inspired by [Koa](https://koajs.com/) and [Express](http://expressjs.com/)

In most web frameworks, routing is done by iterating over a bunch of handlers which can be very inefficient. To solve this problem, PhylumJS splits a route in it's components and organizes handlers in a tree like data structure. In combination with async functions, this provides a robust system for any kind of web application.

<br/>



# Installation
```bash
npm i @phylum/core
```

## Quick Start
```js
import Phylum from '@phylum/core'

const app = new Phylum()

app.get('/', async ctx => {
	ctx.response.body = 'Hello World!'
})

app.listen(8080)
```

<br/>



# Documentation
+ [Routing Concepts](#routing-concepts)
+ [Phylum](#phylum)
	+ [app.data](#appdata)
	+ [app.settings](#appsettings)
	+ [app.listener()](#applistener)
	+ [app.listen(..)](#applisten)
	+ [Event: error](#event-error)
	+ [Event: route-error](#event-route-error)
+ [Router](#router)
	+ [router.use([route, ][options, ]handler)](#routeruseroute-options-handler)
	+ [router.METHOD([route, ][options, ]handler)](#routermethodroute-options-handler)
	+ [router.rewrite([route, ][options, ]handler)](#routerrewriteroute-options-handler)
	+ [router.route(ctx, next)](#routerroutectx-next)
	+ [router.handler()](#routerhandler)
	+ [router.router([route])](#routerrouterroute)
+ [Routes](#routes)
	+ [Route Parameters](#route-parameters)
	+ [Route Options](#route-options)
+ [RouteError](#routeerror)
	+ [new RouteError([options])](#new-routeerroroptions)
	+ [routeErr.err](#routeerrerr)
	+ [routeErr.status](#routeerrstatus)
	+ [routeErr.message](#routeerrmessage)
+ [Context](#context)
	+ [ctx.request](#context)
	+ [ctx.response](#context)
	+ [ctx.app](#context)
	+ [ctx.data](#context)
	+ [ctx.path](#context)
	+ [ctx.params](#context)
	+ [ctx.destroy([err])](#ctxdestroyerr)
	+ [ctx.rewriteTo(target)](#ctxrewritetotarget)
+ [Request](#request)
	+ [request.ctx](#request)
	+ [request.raw](#request)
	+ [request.path](#request)
	+ [request.querystr](#request)
	+ [request.query](#request)
	+ [request.url](#request)
	+ [request.method](#request)
	+ [request.headers](#request)
	+ [request.headerNames](#request)
	+ [request.body](#requestbody)
	+ [request.readable](#requestreadable)
	+ [request.get(name)](#requestgetname)
	+ [request.set(name, value)](#requestgetname-value)
	+ [request.remove(name)](#requestremovename)
	+ [request.has(name)](#requesthasname)
+ [Response](#response)
	+ [response.ctx](#request)
	+ [response.raw](#response)
	+ [response.status](#response)
	+ [response.message](#response)
	+ [response.headerNames](#response)
	+ [response.body](#responsebody)
	+ [response.get(name)](#responsegetname)
	+ [response.set(name, value)](#responsegetname-value)
	+ [response.remove(name)](#responseremovename)
	+ [response.has(name)](#responsehasname)
+ [Headers](#headers)
	+ [type](#type)
	+ [length](#length)
	+ [host](#host)
	+ [encoding](#encoding)
	+ [acceptEncoding](#acceptencoding)
+ [Body Parsers](#body-parsers)
	+ [parserSocket(parsers)](#parsersocketparsers)
	+ [MemoryParser](#memoryparser)
	+ [JsonParser](#jsonparser)
	+ [FormParser](#formparser)
+ [Serving static content](#serving-static-content)
+ [Development](#development)

<br/>



# Routing Concepts
> The following section outlines how the routing tree is traversed for a specific request path.<br/>

For each request path there are specific handlers that are invoked in order. Every handler must return a promise that resolves or rejects after the request has been handled or the promise returned by the *next* function resolved or rejected.

To route a request, the request path is split into it's components. For each possible route *(e.g. '/', '/foo' and '/foo/bar' for the request path '/foo/bar')*, handlers are ordered as follows:
+ handlers for the current route in the order they were registered
+ direct regexp child routes (All routes that are identified by regular expressions)
+ direct fixed child routes (All routes that are identified by fixed paths)

<br/>



# Phylum
The `Phylum` class acts as the main entry point for any routing by accepting requests from node http servers.
```js
import Phylum from '@phylum/core'

const app = new Phylum()
```

### app.data
The prototype for every `ctx.data` object that is created.<br/>
You can pass data to handlers by storing it on this object.
```js
app.data.message = 'Hello World!'

app.get('/', async ctx => {
	ctx.response.body = ctx.data.message
})
```

### app.settings
An object with settings:
+ trustProxy `<boolean>` - True to trust proxy headers. Default is `false`

### app.listener()
Create a listener callback that can be bound to the `request` event of http servers.

### app.listen(...)
API sugar for the following code:
```js
app.listen(8080)
// is the same as:
http.createServer(app.listener()).listen(8080)
```
+ listen arguments are documented [here](https://nodejs.org/dist/latest/docs/api/net.html#net_server_listen)
+ returns the http server that was created.

### Event: 'error'
The `error` event is emitted for any error that occurs.
If the error originates from handling a request, the context will be passed with the second argument.
```js
app.on('error', (err, ctx) => {
	if (ctx) {
		console.error(`Error in: ${ctx.request.url}`, err)
	} else {
		console.error(err)
	}
})
```

### Event: 'route-error'
The `route-error` event is emitted for any `<RouteError>` that occurs.<br/>
This event is meant for debugging or logging.
```js
app.on('route-error', (err, ctx) => {
	console.warn(`Route error: ${ctx.request.url} => ${ctx.response.status}`)
	if (err.err) {
		console.warn('  Caused by: ', err.err)
	}
})
```

<br/>



# Router
The `Router` class routes requests.<br/>
Each `router` is an `EventEitter`.
```js
import {Router} from '@phylum/core'

const router = new Router()
```

### router.use([route, ][options, ]handler)
Attach a handler to the router.
```js
router.use('/foo', ctx => {
	console.log('bar!')
	return next()
})
```
+ route `<Route>` - Optional. The route as explained below. Default is `'/'`
+ options `<object>` - Optional. Route options as explained below. Default is `{}`
+ handler `<function>` - The handler function:
	+ ctx `<Context>` - The context is passed with the first argument.
	+ next `<function>` - A function to pass control to the next handler.
		+ Calling this function indicates that the request was not handled.
		+ returns a promise that resolves when the request was handled or rejects if an error occured.
	+ **always** return a promise by either making the handler function `async` or just returning a promise. Not returning a promise could result in unhandled errors or rejections.

### router.METHOD([route, ][options, ]handler)
The same as `router.use`, but withThis should be used in combination with  `options.method` set to the specific one.<br/>
The following methods are supported:
```js
router.get(..)
router.post(..)
router.put(..)
```

### router.rewrite([route, ][options, ]handler)
Attach a handler to the router to rewrite requests.<br/>
*Note that rewritten requests are fully routed using both the original and the new path.*
```js
// Write all requests to '/foo/...' to '/bar':
router.rewrite('/foo', () => '/bar')
```
+ route `<Route>` - Optional. The route as explained below. Default is `'/'`
+ options `<object>` - Optional. Route options as explained below. Default is `{}`
+ handler `<function>` - The handler function:
	+ ctx `<Context>` - The context is passed with the first argument.
	+ to rewrite a request:
		+ return a `<string>` that denotes the new path.
		+ return an `<array>` of unescaped path components without slashes.

### router.route(ctx, next)
Route a request.<br/>
*This can be used to mount a router. However it is recommended to use router.handler instead.*
```js
const foo = new Router()

app.use('/foo', (ctx, next) => {
	return foo.route(ctx, next)
})
```
+ ctx `<Context>` - The context.
+ next `<function>` - The next function to call if the request was not handled by the router.
+ returns `<Promise>` - A promise.

### router.handler()
Create a handler that can be used to mount the router.
```js
const foo = new Router()

app.use('/foo', foo.handler())

// foo.handler() is a shorthand for:
foo.route.bind(foo)
```
+ returns `<function>` - A handler function.

### router.router([route])
Create a router and mount it.
```js
const foo = app.router('/foo')

// app.router('/foo') is a shorthand for:
const foo = new Router()
app.use(foo.handler())
```
+ route `<Route>` - Optional. The route to mount the new router.
+ returns `<Router>` - The mounted router.

<br/>



# Routes
Routes are used to specify the request path, a handler is responsible for.<br/>
A route can be any of the following:
+ `<string>` - A route path. This path may contain slashes.
+ `<RegExp>` - A regular expression to match against a single request path component.
+ `<Array>` - A combination of any of this three types.

The following are some example routes.<br/>
```js
// Route matching '/foo/bar':
'/foo/bar'
['foo', 'bar']
['foo/bar']

// Route matching '/example/foo' and '/example/bar':
['example', /^(?:foo|bar)$/]
```

### Route Parameters
Route parameters can be defined using named regular expression groups.
```js
// Route matching '/greet/bob'
['greet', /(?<name>\w+)/]
```
Matched results can be accessed via `ctx.params`
```js
ctx.params.name // 'bob'
```

### Route Options
The following route options are available:
```js
{
	method: 'GET'
}
```
+ method `<string>` - If specified, the route only applies when the request method matches and there is no rest path to route.

<br/>



# RouteError
This error can be thrown by handlers or middleware to indicate that something was wrong with the request.<br/>
*Note that a `<RouteError>` is not an `<Error>`!*<br/>

If a route error is catched by middleware, the response status code and message should be set according to the error. If a route error is not catched, the application will emit an `route-error` event and set the status code and message.
```js
import {RouteError} from '@phylum/core'
```

### new RouteError([options])
```js
new RouteError({
	err: someErrorThatCausedThis,
	status: 418,
	message: 'I am a teapot'
})
```
+ options `<object>` - An optional object with the following properties:
	+ err `<any>` - Optional. An error that caused the route error.
	+ status `<number>` - Optional. The status code to set. Default is `400`
	+ message `<string>` - Optional. The status message to set. Default is `undefined`

### routeErr.err
The error that caused the route error if any or undefined.

### routeErr.status
The status code.

### routeErr.message
The status message if any or undefined.

<br/>



# Context
A context represents a unique request and is passed to handler functions.

+ ctx.request `<Request>` - The request object.
+ ctx.response `<Response>` - The response object.
+ ctx.app `<Phylum>` - A reference to the phylum instance.
+ ctx.data `<object>` - An object that can be populated with custom data. The prototype of this object can be accessed with `app.data`.
+ ctx.path `<Array>` - An array of uri decoded rest path components that represents the rest path to route.
+ ctx.params `<object>` - An object that is populated with route parameters.

### ctx.destroy([err])
Destroy the underlying socket.<br/>
If an error is specified, an `clientError` event will be emitted on the http server.
```js
router.get('/foo', async ctx => {
	ctx.destroy(new Error('some error...'))
})
```
+ err `<any>` - An optional error to emit a `clientError` event on the http server.

*It recommended to use route errors instead to give middleware the chance to manipulate the way errors like parsing errors are handled.*

### ctx.rewriteTo(target)
Construct a path of specified target plus the current rest path.<br/>
The result can be used with `router.rewrite(..)`
```js
router.get('/bar/example', async ctx => {
	ctx.response.body = 'Hello World!'
})

router.rewrite('/foo', ctx => {
	return ctx.rewriteTo('/bar')
})

// GET /foo/example or /bar/example => 'Hello World!'
```

<br/>



# Request
The `ctx.request` object is a thin wrapper around the original request object.

+ request.ctx `<Context>` - A circular reference to the context.
+ request.raw `<http.IncomingMessage>` - The original http request.
+ request.path `<string>` - The path part of the request uri without the query string.
+ request.querystr `<string>` - The query string without '?' or an empty string.
+ request.query `<object>` - The query string parsed using `querystring.parse` or an empty object.
+ request.url `<string>` - Get the original request url.
+ request.method `<string>` - Get the request method.
+ request.headers `<object>` - The [header object](https://nodejs.org/dist/latest/docs/api/http.html#http_message_headers).
+ request.headerNames `<Array>` - An array with lower cased header names.

### request.body
The request body. This property can be set by body parsers.<br/>
`undefined` indicates that the body has not been parsed yet.

### request.readable
A readable stream for parsing the request body. By default, this is the original http request object. However it can be replaced by decompression middleware.<br/>
Note that the stream may emit an `aborted` instead of an `error` event, so body parsers must listen for both events. If an error occurs while parsing, the parser should call `ctx.destroy` with the error.

### request.get(name)
Get a request header.
```js
ctx.request.get('Content-Type')
```
+ name `<string>` - The case insensitive header name.
+ returns `<string> | <Array> | undefined` - The header or undefined. The type [depends on the header](https://nodejs.org/dist/latest/docs/api/http.html#http_message_headers).

### request.set(name, value)
Set or replace a request header.
```js
ctx.request.set('Content-Type', 'application/json; charset=utf8')
```
+ name `<string>` - The case insensitive header name.
+ value - The type of the value depends on the header name:
	+ must be a `<string>` for the following headers: `age, authorization, content-length, content-type, etag, expires, from, host, if-modified-since, if-unmodified-since, last-modified, location, max-forwards, proxy-authorization, referer, retry-after, user-agent`
	+ must be an `<Array>` for the `set-cookie` header.
	+ For all other headers, value can be a `<string>` or an `<array>` that will be joined with `', '`

### request.remove(name)
Remove a request header.
```js
ctx.request.remove('Content-Type')
```
+ name `<string>` - The case insensitive header name.

### request.has(name)
Check if a request header is set.
```js
ctx.request.has('Content-Type')
```
+ name `<string>` - The case insensitive header name.
+ returns `<boolean>` - True if the header is set.

<br/>



# Response
The `ctx.response` object is a thin wrapper around the original response object.

+ response.ctx `<Context>` - A circular reference to the context.
+ response.raw `<http.ServerResponse>` - The original http response.
+ response.status `<number>` - Get or set the status code.
+ response.message `<string>` - Get or set the status message.
+ response.headerNames `<Array>` - Get an array with lower cased header names.

### response.body
The response body to send when the request has been routed.
```js
ctx.response.body = 'Hello World!'
```
+ `null, undefined` - Send an empty response body.
+ `<string>` - Send data utf8 encoded.
+ `<Buffer>` - Send binary data.
+ `<stream.Readable>` - Will be piped to the response stream.

### response.get(name)
Get a response header.
```js
ctx.response.get('Content-Type')
```
+ name `<string>` - The case insensitive header name.
+ returns `<string> | <Array> | undefined` - The header value or undefined if not set.

### response.set(name, value)
Set or replace a response header.
```js
ctx.response.set('Content-Type', 'application/json; charset=utf8')
```
+ name `<string>` - The case insensitive header name.
+ value `<string> | <Array>` - The header value. This should be a string for a single header or an array for multiple headers with the same name.

### response.remove(name)
Remove a response header.
```js
ctx.response.remove('Content-Type')
```
+ name `<string>` - The case insensitive header name.

### response.has(name)
Check if a response header is set.
```js
ctx.response.has('Content-Type')
```
+ name `<string>` - The case insensitive header name.
+ returns `<boolean>` - True if the header is set.

<br/>



# Headers
The following properties can be used to get or set http headers.

### type
Get the `Content-Type` header:
```js
request.type // -> {mime: 'application/json', charset: 'utf-8'}
```
+ returns `undefined` if the header is not set.
+ returns an object with the following properties:
	+ mime `<string>` - The mime type like `'application/json'`.
	+ charset `<string>` - Optional. The charset parameter.
	+ boundary `<string>` - Optional. The boundary parameter.

### type=
Set the `Content-Type` header:
```js
response.type = 'json'
response.type = 'application/json'
response.type = 'application/json; charset=utf-8'
response.type = {mime: 'application/json', charset: 'utf-8'}
```
+ `<string>` - The mime string or file extension (without dot). In this case, the charset will be set automatically. If the value is not a known mime type or file extension, the header is set to the unmodified value.
+ `<object>` - An object with the following properties:
	+ mime `<string>` - The exact mime type like `'application/json'`.
	+ charset `<string>` - Optional. The charset. If not specified, no charset parameter will be appended.
	+ boundary `<string>` - Optional. The boundary parameter.
+ `<falsy>` - Set to any falsy value to remove the header.

### length
Get the `Content-Length` header:
```js
request.length // -> 17
```
+ returns `undefined` if the header is not set.
+ returns `<number>` if the header exists or `NaN` if the header could not be parsed.

### length=
Set the `Content-Length` header:
```js
response.length = 42
```
+ `<number>` - The content length.
+ `<falsy>` - Any falsy value except 0 to remove the header.

### host
Get the `Host` header:<br/>
*If `app.settings.trustProxy` is true, the `X-Forwarded-Host` header is preferred.*
```js
request.host // -> 'example.com'
```
+ returns `undefined` if the header is not set (which would be very unusual)
+ returns a `<string>` that includes the address and an optional port. Additional hosts are ignored.

*Note that this header can only be used on the request object.*

### host=
Set the `Host` header:<br/>
*If `app.settings.trustProxy` is true, the `X-Forwarded-Host` header is removed automatically when the host is set.*
```js
request.host = 'example.com'
```
+ `<string>` - The host header.

### encoding
Get the `Content-Encoding` header:
```js
request.encoding // -> ['gzip']
```
+ returns `undefined` if the header is not set.
+ returns an `<array>` with the encoding labels.

### encoding=
Set the `Content-Encoding` header:
```js
response.encoding = null
response.encoding = 'gzip'
response.encoding = ['gzip']
```
+ `<falsy>` - Any falsy value (including an empty string) to remove the header.
+ `<string>` - The raw header value. This should contain comma-seperated content encoding labels.
+ `<Array>` - An array with content encoding labels.

### acceptEncoding
The same as `encoding`, but for the `Accept-Encoding` header.

<br/>



# Body Parsers
Body parsers are mounted using a so called *parser socket*, which selects a parser based on the content type header. The parsed request body is stored on the `request.body` property.

### parserSocket(parsers)
Create a parser socket middleware with the specified parsers.<br/>
For every request without an already parsed request body, the socket checks, if a parser can parse the request body and invokes it's parse function.
```js
import {parserSocket} from '@phylum/core'

app.use(parserSocket(parsers))
```
+ parsers `<array>` - An array with parsers to use. Every parser must implement the following api:
	+ mimes `<array>` - An array of mime type strings like `'application/json'` the parser is responsible for.
	+ parse(ctx, next, type) - A function to parse a request body which is basically a route handler function with an additional type argument:
		+ type `<object>` - The parsed `Content-Type` header. See [host header](#host)

### MemoryParser
The memory parser stores the request body in memory as strings or buffers.
```js
import {MemoryParser} from '@phylum/core'

new MemoryParser({
	mimes: ['text/plain'],
	encoding: 'utf8'
})
```
+ mimes `<Array>` - An array of mime type strings the parser is responsible for.
+ encoding `<string>` - Optional. If falsy, data will be stored as a buffer (which is the default). If specified, data will be stored as a string. If the `Content-Type` header specifies a charset, that charset is used instead.

### JsonParser
Parses the request body as json.
```js
import {JsonParser} from '@phylum/core'

new JsonParser()
new JsonParser({
	mimes: ['application/json'],
	encoding: 'utf8'
})
```
+ mimes `<Array>` - Optional. An array of mime type strings the parser is responsible for. Default is `['applicaion/json']`
+ encoding `<string>` - Optional. The encoding to use if no charset is specified by the `Content-Type` header. Default is `'utf8'`

### FormParser
Parses the request body as url encoded data.
```js
import {FormParser} from '@phylum/core'

new FormParser()
new FormParser({
	mimes: ['application/x-www-form-urlencoded'],
	encoding: 'utf8'
})
```
+ mimes `<Array>` - Optional. An array of mime type strings the parser is responsible for. Default is `['applicaion/x-www-form-urlencoded']`
+ encoding `<string>` - Optional. The encoding to use if no charset is specified by the `Content-Type` header. Default is `'utf8'`

*Note, that this parser can only parse url encoded data. `multipart/form-data` is not supported by the core library.*

<br/>



# Serving static content
> *Please note that this feature is experimental!*

Static content can be served using the `staticContent` middleware.
```js
import {staticContent} from '@phylum/core'

app.use(staticContent(options))
```
+ options `<object>` - An object with the following options:
	+ root `<string>` - The root path. This option is required.
	+ index `<array>` - Optional. An array with names to use when a directory is requested. Default is `['index.html']`
	+ type `<boolean>` - True to send a content type header. Default is `true`

### Use in production
When serving static content from your node application in production you should consider at least one of the following options:
+ Serve static files using a *cdn*.
+ Serve static files through a http server like nginx.
+ Use a reverse proxy (like nginx) for caching as phylum itself does not apply any caching.

<br/>



# Development
```bash
git clone https://github.com/phylumjs/core phylum-core
cd phylum-core
npm i
```

### Tests
```bash
# Run tests and get coverage results:
npm test

# Run tests and watch for changes:
npm run dev
```