JSPM

  • Created
  • Published
  • Downloads 20
  • Score
    100M100P100Q93054F
  • License MIT

WebAPI charged HTTP2-ready web server for node.js and browser

Package Exports

  • @plant/plant

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@plant/plant) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Plant logo

npm Travis npm


Plant is WebAPI standards based web server powered by ES2017, created with modular architecture and functional design patterns in mind. It uses cascades and contexts to be modular, pure and less coupled.

Plant is transport agnostic and can work right in the browser using WebSockets or event PostMessage.

Features

  • 🏎 Faster then Express: 15K vs 14K req/sec on Hello World test.
  • ☁️ Lightweight: 71 KiB with comments and 28 KiB when minified.
  • 📐 Standards based: uses WebAPI URL and Headers interfaces.

Table of Contents


Install

Production version:

npm i @plant/plant

Or development version:

npm i @plant/plant@next

Usage

Plant is abstract web server so it has no builtin transport. It depends on modules for http, https, ws or even rpc to provide transport layer. In this example https is using so it package should to be installed (npm i @plant/https).

const createServer = require('@plant/http');
const Plant = require('@plant/plant');

const plant = new Plant();

// Send text response
plant.use(async function({res}) {
    res.body = 'Hello World';
});

// Build request handler
createServer(plant)
.listen(8080);

Examples

Context

The default context has this properties:

  • reqRequest instance.
  • resResponse instance.
  • peerPeer representing other connection party.
  • socketSocket is abstract socket, representing connection.
  • subRequestsubRequest() method to send request to the server from handlers.

Cascades explanation

Cascades are nested functions which passes context object to the deepest function. The flow and depth could be modified using or and and modifiers. Each level of cascade could modify context on it's own without touching overlaying or adjacent context.

Default context contains req, res and socket properties. You can add your own properties, modify or delete existing:

plant.use(async function({req, res, socket}, next) => {
    await next({}); // Set context empty
});

plant.use(async (ctx, next) => {
    ctx; // -> {}
    await next({number: 3.14}); // Create new context with `number` property
});

plant.use(async (ctx, next) => {
    ctx; // -> {number: 3.14}
    await next(); // No context modification
});

It allow to create predictable behavior and avoid unexpected side effects to change. Plant itself overwrite default node.js HTTP Request and Response objects with Plant.Request and Plant.Response.

API

Plant Type

Plant is the handlers configuration and flow manipulation instrument. It allow to specify execution order, define routes and set uncaught error handler. It has no readable props.

Plant.constuctor()

([options:PlantOptions]) -> Plant

PlantOptions Type

{
    handlers: Handlers[] = [],
    errorHandler: (Error) -> void = console.log,
    context: Object = {},
}

Plant server configuration options.

Property Description
handlers Array of request handlers added to cascade
errorHandler This error handler will capture unhandled error when response is send
context Default context values. Empty object by default

Plant.use()

([route:String], ...handlers:Handler) -> Plant

This method do several things:

  1. If route specified add route.
  2. If handler count greater than one it creates turn for request which allow to change Request execution direction.
Example
function conditionHandler({req}, next) {
    if (req.url.searchParams.has('n')) {
        return next();
    }
}

plant.use('/a', conditionHandler, ({res}) => res.text('n param passed'));
plant.use('/a', ({res}) => res.text('n param not passed'));

Plant.or()

(...handlers: Handler) -> Plant

Add handlers in parallel. Plant will iterate over handler until response body is set.

Example
plant.or(
    // Executed. Send nothing, so go to the next handler.
    ({req}) => {},
    // Executed. Send 'ok'.
    ({res}) => { res.body = 'ok'; },
    // Not executed. Previous handler set response body.
    ({req}) => {}
);

Plant.and()

(...handlers:Handle) -> Plant

This method set new cascades. It's the same as call use for each handler.

Example
function add({i = 0, ctx}, next) {
    return next({...ctx, i: i + 1});
}

// Define cascade
plant.and(add, add, add, ({i, res}) => res.text(i)); // i is 3

Plant.handler()

() -> http.RequestListener

This method returns requestListener value for native node.js http/https server:

Example
http.createServer(plant.handler())
.listen(8080);

Handler Type

This type specify cascadable function or object which has method to create such function.

const Router = require('@plant/router');

const router = new Router();
router.get('/', ({res}) => {
    res.body = 'Hello';
});

server.use(router.handler());

Peer Type

{
    uri: URI
}

This type represents other side of request connection. It could be user or proxy. It could be non unique for each request if the peer has sent several requests using the same connection.

For local TCP connection it could look like this:

new Peer({
    uri: new URI({
        protocol: 'tcp:',
        hostname: '127.0.0.1',
        port: 12345,
    })
})

Request Type

{
    url: URL,
    method: String,
    headers: Headers,
    domains: String[],
    body: ReadableStream|null,
    buffer: ArrayBuffer|null,
}
Property Description
url Url is a WebAPI URL
method HTTP method
headers WebAPI Headers object
domains Domains name separated by '.' in reverse order
body Request body readable stream. It is null by default if body not exists (GET, HEAD, OPTIONS request).
buffer If body has been read already this property will contain a buffer
parent non-standard Request that caused current request to be called. For example for http2 push

Request.Request()

(options:RequestOptions) -> Request

Creates and configure Request instance. Headers passed to request object should be in immutable mode.

RequestOptions

{
    method: String='GET',
    url: URL,
    headers: Object|Headers={},
    body: ReadableStream|Null=null,
    parent: Request|Null = null,
}

Request.is()

(type:String) -> Boolean

Determine if request header 'content-type' contains type. Needle type can be a mimetype('text/html') or shorthand ('json', 'html', etc.).

This method uses type-is package.

Request.type()

(types:String[]) -> String|Null

Check if content-type header contains one of the passed types. If so returns matching value either returns null.

Example
switch(req.type(['json', 'multipart'])) {
    case 'json':
        req.data = JSON.parse(req.body);
        break;
    case 'multipart':
        req.data = parseMultipart(req.body);
        break;
    default:
        req.data = {};
}

Request.accept()

(types:String[]) -> String|Null

Check if accept header contains one of the passed types. If so returns matching value otherwise returns null.

Example
switch(req.accept(['json', 'text'])) {
    case 'json':
        res.json({result: 3.14159});
        break;
    case 'text':
        res.text('3.14159');
        break;
    default:
        res.html('<html><body>3.14159</body></html>');
}

Request.arrayBuffer()

() -> Promise<Uint8Array,Error>

Read request body and returns it as an Uint8Array.

Request.blob()

() -> Promise<Blob,Error>

⚠️ Not implemented yet

Read request body and returns it as a Blob.

Request.formData()

() -> Promise<FormData,Error>

⚠️ Not implemented yet

Read request body and returns it as a FormData.

Request.json()

() -> Promise<*,Error>

Read request body and parse it as JSON.

Request.text()

() -> Promise<string,Error>

Read request body and returns it as a string.

Response Type

{
    ok: Boolean,
    hasBody: Boolean,
    status: Number,
    headers: Headers,
    body: Blob|ArrayBuffer|ReadableStream|String|Null,
}
Property Description
ok True if status is in range of 200 and 299
hasBody True if body is not null. Specify is response should be sent
status Status code. 200 By default
headers Response headers as WebAPI Headers object
body Response body. Default is null

Response.Response()

(options:ResponseOptions) -> Request

Creates and configure response options. Headers passed as WebAPI instance should have mode 'none'.

ResponseOptions

{
    status: Number=200,
    headers: Headers|Object={},
    body: Buffer|ReadableStream|String|Null=null,
}

Response.setStatus()

(status:number) -> Response

Set response status property.

Example
res.setStatus(200)
.send('Hello');

Response.redirect()

(url:String) -> Response

Redirect page to another url. Set empty body.

Example
res.redirect('../users')
.text('Page moved');

Response.json()

(json:*) -> Response

Send JS value as response with conversion it to JSON string. Set application/json content type.

res.json({number: 3.14159});

Response.text()

(text:String) -> Response

Send text as response. Set text/plain content type.

Response.formData()

⚠️ Not implemented yet.

Example
res.text('3.14159');

Response.html()

(html:String) -> Response

Send string as response. Set text/html content type.

Example
res.html('<html><body>3.14159</body></html>');

Response.stream()

(stream:Readable) -> Response

Send Readable stream in response.

Example
res.headers.set('content-type', 'application/octet-stream');
res.stream(fs.createReadStream(req.path));

Response.send()

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

Set any string-like value as response.

Response.end()

() -> Response

Set empty body.

Headers Type

{
    mode: String=Headers.MODE_NONE
}
Property Description
mode Headers mutability mode

Plant is using WebAPI Headers for Request and Response.

// Request headers
plant.use(async function({req}, next) {
    if (req.headers.has('authorization')) {
        const auth = req.headers.get('authorization');
        // Process authorization header...
    }

    await next();
});

// Response headers
plant.use(async function({req, res}, next) {
    res.headers.set('content-type', 'image/png');
    res.send(fs.createReadStream('logo.png'));
});

Request Headers object has immutable mode (Headers.MODE_IMMUTABLE) and according to specification it will throw each time when you try to modify it.

Headers.MODE_NONE

String='none'

Constant. Default Headers mode which allow any modifications.

Headers.MODE_IMMUTABLE

String='immutable'

Constant. Headers mode which prevent headers from modifications.

Headers.Headers()

(headers:HeadersParam, mode:String=Headers.MODE_NONE) -> Headers

Constructor accepts header values as object or entries and mode string. Request headers always immutable so Request.headers will always have MODE_IMMUTABLE mode value.

HeadersParam Type

Object.<String,String>|Array.<Array.<String, String>>
Example
const headers = new Headers({
    'content-type': 'text/plain',
}, Headers.MODE_IMMUTABLE);
// ... same as ...
const headers = new Headers([
    ['content-type', 'text/plain'],
]);

Headers.raw()

(header:String) -> String[]

Nonstandard. Returns all header values as array. If header is not set returns empty array.

Socket Type

{
    isEnded: Boolean = false,
}

Socket wraps connection and allow disconnect from other side when needed. To stop request call socket.end(). This will prevent response from be sent and close connection. All overlay cascades will be executed, but response will not be sent.

Socket.Socket()

({onEnd:() -> void}) -> Socket

Constructor has one only option onEnd which is a function called when connection ended.

Socket.canPush

Boolean

Determine wether socket allows to push responses.

Socket.isEnded

Boolean

Property specifies whether socket is ended. Using to prevent response from sending and cascade from propagation.

Socket.end()

() -> void

End connection. Call onEnd function passed into constructor.

Socket.destroy()

() -> void

⚠️ It should not be called in handlers. This method is for low level request handlers only.

Destroy connection and remove events listeners.

Socket.push()

(response: Response) -> Promise<void,Error>

Push response to the client. If it's supported.

subRequest()

(request:Request|requestOptions) -> Subrequest

Subrequest factory method. Receives request instance or request constructor arguments. Returns Subrequest type.

Subrequest Type

Subrequest#context()

(context: Object) -> Subrequest

Set initial context values for subrequest.

Subrequest#send()

() -> Promise<Response,Error>

Make a call and return response for specified request. Returns promise that fulfills with Response instance.

Subrequest#push()

() -> Promise<Response,Error>

Make a call and immediately push response for specified request. Returns promise that fulfills with Response instance.

Error handling

Async cascade model allow to capture errors with try/catch:

async function errorHandler({req, res}, next) {
    try {
        await next(); // Run all underlaying handlers
    }
    catch (error) {
        res.status(500);

        if (req.is('json')) {
            res.json({
                error: error.message,
            });
        }
        else {
            res.text(error.message);
        }
    }
};

URI Type

URI is an object that represents URI in plant. While URL requires protocols to be registered by IANA, WebAPI URL wouldn't parse strings with custom scheme like tcp://127.0.0.1:12345/ (127.0.0.1:12345 became a part of pathname). Thus we use URI, which doesn't mean to be an URL, but presents network identifier correct. Plant doesn't provide parser and URI should be generated manually.

This is how Plant represents TCP address of the HTTP peer:

new URI({
    protocol: 'tcp:',
    hostname: 'localhost',
    port: '12345',
    pathname: '/',
})

This implementation will be enhanced with parser in the next versions.

Comparison

Plant is mostly the same as Koa but it has its' own differences.

Difference from Koa

Plant is trying to be more lightweight like Connect and to have complete interface like Express. It uses async cascades like in Koa, but plant's context has other nature. Plant's context is plain object (not a special one) and it could be modified while moving through cascade but only for underlaying handlers:

async function sendVersion({res, v}) {
    res.text(`version: ${v}`);
}

plant.use('/api/v1', async function(ctx, next) {
    ctx.v = 1;
    // Update ctx
    await next(ctx);
}, sendVersion); // This will send `version: 1`

plant.use('/api/v2', async function(ctx, next) {
    ctx.v = 2;
    // Update ctx
    await next(ctx);
}, sendVersion); // This will send `version: 2`

plant.use(sendVersion); // This will send `version: undefined`

Also plant is using express-like response methods: text, html, json, send:

plant.use(async function({req, res}) {
    res.send(req.stream);
});

Difference from Express

Well middlewares are calling handlers (because it shorter). Plant is an object (not a function). Plant could not listening connection itself and has no listen method for that. Request and Response objects are not ancestors of native Node.js's http.IncomingMessage and http.ServerResponse.

Domains instead of subdomains

Request object has domains property instead of subdomains and has all parts of host from tld zone:

req.domains; // -> ['com', 'github', 'api'] for api.github.com

No extension

Plant doesn't extends Request or Response object with new methods. It's using context which be modified and extended with new behavior.

License

MIT © Rumkin