0.1.3 • Published 5 years ago

@phylum/core v0.1.3

Weekly downloads
1
License
MIT
Repository
github
Last release
5 years ago

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 and Express

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.

Installation

npm i @phylum/core

Quick Start

import Phylum from '@phylum/core'

const app = new Phylum()

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

app.listen(8080)

Documentation

Routing Concepts

The following section outlines how the routing tree is traversed for a specific request path.

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)

Phylum

The Phylum class acts as the main entry point for any routing by accepting requests from node http servers.

import Phylum from '@phylum/core'

const app = new Phylum()

app.data

The prototype for every ctx.data object that is created. You can pass data to handlers by storing it on this object.

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:

app.listen(8080)
// is the same as:
http.createServer(app.listener()).listen(8080)
  • listen arguments are documented here
  • 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.

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. This event is meant for debugging or logging.

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)
	}
})

Router

The Router class routes requests. Each router is an EventEitter.

import {Router} from '@phylum/core'

const router = new Router()

router.use(route, handler)

Attach a handler to the router.

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, handler)

The same as router.use, but withThis should be used in combination with options.method set to the specific one. The following methods are supported:

router.get(..)
router.post(..)
router.put(..)

router.rewrite(route, handler)

Attach a handler to the router to rewrite requests. Note that rewritten requests are fully routed using both the original and the new path.

// 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. This can be used to mount a router. However it is recommended to use router.handler instead.

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.

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.

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.

Routes

Routes are used to specify the request path, a handler is responsible for. 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.

// 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.

// Route matching '/greet/bob'
['greet', /(?<name>\w+)/]

Matched results can be accessed via ctx.params

ctx.params.name // 'bob'

Route Options

The following route options are available:

{
	method: 'GET'
}
  • method <string> - If specified, the route only applies when the request method matches and there is no rest path to route.

RouteError

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

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.

import {RouteError} from '@phylum/core'

new RouteError(options)

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.

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. If an error is specified, an clientError event will be emitted on the http server.

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. The result can be used with router.rewrite(..)

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!'

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.
  • request.headerNames <Array> - An array with lower cased header names.

request.body

The request body. This property can be set by body parsers. 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. 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.

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.

request.set(name, value)

Set or replace a request header.

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.

ctx.request.remove('Content-Type')
  • name <string> - The case insensitive header name.

request.has(name)

Check if a request header is set.

ctx.request.has('Content-Type')
  • name <string> - The case insensitive header name.
  • returns <boolean> - True if the header is set.

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.

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.

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.

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.

ctx.response.remove('Content-Type')
  • name <string> - The case insensitive header name.

response.has(name)

Check if a response header is set.

ctx.response.has('Content-Type')
  • name <string> - The case insensitive header name.
  • returns <boolean> - True if the header is set.

Headers

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

type

Get the Content-Type header:

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:

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:

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:

response.length = 42
  • <number> - The content length.
  • <falsy> - Any falsy value except 0 to remove the header.

host

Get the Host header: If app.settings.trustProxy is true, the X-Forwarded-Host header is preferred.

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: If app.settings.trustProxy is true, the X-Forwarded-Host header is removed automatically when the host is set.

request.host = 'example.com'
  • <string> - The host header.

encoding

Get the Content-Encoding header:

request.encoding // -> ['gzip']
  • returns undefined if the header is not set.
  • returns an <array> with the encoding labels.

encoding=

Set the Content-Encoding header:

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.

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. 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.

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

MemoryParser

The memory parser stores the request body in memory as strings or buffers.

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.

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.

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.

Serving static content

Please note that this feature is experimental!

Static content can be served using the staticContent middleware.

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.

Development

git clone https://github.com/phylumjs/core phylum-core
cd phylum-core
npm i

Tests

# Run tests and get coverage results:
npm test

# Run tests and watch for changes:
npm run dev