@phylum/core
v0.1.3
Published
PhylumJS Web Server Core
Downloads
2
Maintainers
Readme
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
- Phylum
- Router
- Routes
- RouteError
- Context
- Request
- Response
- Headers
- Body Parsers
- Serving static content
- Development
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 isfalse
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, ][options, ]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.
- ctx
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.
The following methods are supported:
router.get(..)
router.post(..)
router.put(..)
router.rewrite([route, ][options, ]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.
- return a
- ctx
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 is400
- message
<string>
- Optional. The status message to set. Default isundefined
- err
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 withapp.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 aclientError
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 usingquerystring.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 theset-cookie
header. - For all other headers, value can be a
<string>
or an<array>
that will be joined with', '
- must be a
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.
- mime
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.
- mime
<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 orNaN
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 parsedContent-Type
header. See host header
- type
- mimes
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 theContent-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 theContent-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 theContent-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 istrue
- root
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