JSPM

  • Created
  • Published
  • Downloads 13
  • Score
    100M100P100Q93659F
  • License MIT

Web application framework

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

npm Travis npm


Plant is WhatWG standards based web server, powered by ES2017, created with modular architecture in mind and functional design patterns on practice. It uses cascades (an isolated customizable contexts) to be modular and clear.

💪 Features

  • Faster then Express on Hello World test 15K vs 14K req/sec.
  • Lightweight: 56Kb with jsdoc comments.
  • WhatWG standards based.

Install

Production version from NPM registry:

npm i @plant/plant

Latest dev version from github:

npm i rumkin/plant

Usage

Plant is using cascades: independent modifiable context protected from intersection.

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

const plant = new Plant();

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

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

Examples

Cascades

Cascades is is isolated scopes presented with Context objects. Each level of cascade could modify context on it's own without touching overlaying context.

Default context contains req, res and socket items. And you can specify your own context to underlaying cascades:

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

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

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

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

Gzip example

Cascades allow to process responses before sending. For example you can gzip response body:

plant.use(async ({req, res}, next) => {
    // Process request
    await next();

    // Create gzip encoder.
    const gzip = zlib.createGzip();
    // Get response body
    const {body} = res;
    // Set Gzip encoding
    res.headers.set('content-encoding', 'gzip');
    // Replace body with stream
    res.stream(gzip);
    // Write data to gzip and close stream.
    gzip.end(body);
});

Router

Plant designed to be API and WebApps ready. So it provide router out from the box.

const Plant = require('@plant/plant');
const {Router} = Plant;

const plant = new Plant();

// Greeting manager
class GreetManager {
    constructor(user) {
        this.user = user;
    }

    greet() {
        return `Hello, ${this.user}`;
    }
}

// Greeting manager router
function greetingRouter(manager) {
    const router = new Router();

    router.get('/', ({res}) => {
        res.body = manager.greet();
    });

    return router;
}

plant.use('/guest', greetingRouter(new GreetManager('guest')));
plant.use('/admin', greetingRouter(new GreetManager('Admin')));
plant.use('/world', greetingRouter(new GreetManager('World')));

Routers are stackable too so it's possible to combine them into complex router.

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

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

// This ...
plant.and(add, add, add, ({i, res}) => res.send(i)); // i is 3

// ... is same as call `use` serially:
plant.use(add);
plant.use(add);
plant.use(add);
plant.use(({i, res}) => res.send(i)); // i is 3

Plant.router()

(route:Router, routes:RouterOptions) -> Plant

Route method initialize new router with params and add it into cascade.

Example
// Few useful handlers
function handleGetUser({req, res}) { /* get user and return response */ }
function handleUpdateUser({req, res}) {/* update user and return response */}

// Configure with routes mapping.
plant.router({
    'get /users/:id': handleGetUser,
    'put /users/:id': handleUpdateUser,
});

// Configure with factory function
plant.router((router) => {
    router.get('/users/:id', handleGetUser);
    router.put('/users/:id', handleUpdateUser);
});

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 = new Router();
router.get('/', ({res}) => {
    res.body = 'Hello';
});

server.use(router.handler());

Router Type

Router allow to group url-dependent functions and extract params from URL.

Example
const plant = new Plant();
const router = new Plant.Router;

router.get('/', () => { /* get resource */ });
router.post('/', () => { /* post resource */ });
router.delete('/', () => { /* delete resource */ });

plant.use(router);

Router.all()

(url:String, ...handlers:Handle) -> Router

Method to add handler for any HTTP method.

Router.get(), .post(), .put(), .patch(), .delete(), .head(), .options()

(url:String, ...handlers:Handle) -> Router

Methods to add handler for exact HTTP method.

Example
router.get('/users/:id', () => {});
router.post('/users/', () => {});
router.put('/users/:id', () => {});
router.delete('/users/:id', () => {});
// ...

Router.route()

(route:String, ...handlers:Router) -> Router

Add handlers into routes queue as new router. Subrouter will add matched url to basePath and reduce path. This is important for nested routers to receive url without prefix.

Example
router.route('/user', ({req}) => {
    req.path; // -> '/'
    req.basePath; // -> '/user'
});
router.get('/user', ({req}) => {
    req.path; // -> '/user'
    req.basePath; // -> '/'
});

Request Type

{
    url: URL,
    method: String,
    headers: Headers,
    sender: String,
    domains: String[],
    path: String,
    basePath: String,
    body: Buffer|Null,
    data: Object,
    stream: Stream,
}
Property Description
url Url is a WhatWG URL
method Lowercased HTTP method
headers WhatWG Headers object
sender Request sender URI. Usually it is an client IP address
domains Domains name separated by '.' in reverse order
path Current unprocessed pathname part
basePath Pathname part processed by overlaying handler
body Request body. It is null by default before body reading
data Data contains values passed within body JSON or Multipart Form
stream Is Readable stream of Request body

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:String|URL,
    headers:Object|Headers = {},
    sender:String,
    body:Buffer|Null=null,
    data:Object={},
    stream:Readable|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 either returns null.

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

Response Type

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

Response.Response()

(options:ResponseOptions) -> Request

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

ResponseOptions

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

Response.status()

(statusCode:number) -> Response

Set response statusCode property.

Example
res.status(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.

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

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

Difference from Koa

Plant tries to be more lightweight like connect and has simple interface like express. It uses async cascades like in Koa, but plant's context has other nature. Plant's context is customizable but isolated. It passed with next call:

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 1

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

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

The first: middlewares are called handlers. Plant is an object (not a function), it has no listen method at all. Request and Response objects are not ancestors of http.IncomingMessage and http.ServerResponse.

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

Other custom behaviour

Request method property value is lowercased:

req.method; // -> 'get'

License

MIT.

© Rumkin 2017-2018