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 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/plantLatest dev version from github:
npm i rumkin/plantUsage
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
- Hello World.
- Echo.
- Cookie handling.
- File serving.
- Response Gzip compression.
- Context separations.
- Session.
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]) -> PlantPlantOptions 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) -> PlantThis method do several things:
- If route specified add route.
- 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) -> RouterAdd 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) -> PlantThis 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) -> PlantRoute 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.RequestListenerThis 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) -> RouterMethod to add handler for any HTTP method.
Router.get(), .post(), .put(), .patch(), .delete(), .head(), .options()
(url:String, ...handlers:Handle) -> RouterMethods 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) -> RouterAdd 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) -> RequestCreates 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) -> BooleanDetermine 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|NullCheck 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|NullCheck 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) -> RequestCreates 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) -> ResponseSet response statusCode property.
Example
res.status(200)
.send('Hello');Response.redirect()
(url:String) -> ResponseRedirect page to another url. Set empty body.
Example
res.redirect('../users')
.text('Page moved');Response.json()
(json:*) -> ResponseSend JS value as response with conversion it to JSON string. Set application/json content type.
res.json({number: 3.14159});Response.text()
(text:String) -> ResponseSend text as response. Set text/plain content type.
Example
res.text('3.14159');Response.html()
(html:String) -> ResponseSend string as response. Set text/html content type.
Example
res.html('<html><body>3.14159</body></html>');Response.stream()
(stream:Readable) -> ResponseSend 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) -> ResponseSet any string-like value as response.
Response.end()
() -> ResponseSet 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) -> HeadersConstructor 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}) -> SocketConstructor has one only option onEnd which is a function called when
connection ended.
Socket.isEnded
BooleanProperty specifies whether socket is ended. Using to prevent response from sending and cascade from propagation.
Socket.end()
() -> voidEnd 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 2Also 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.comOther custom behaviour
Request method property value is lowercased:
req.method; // -> 'get'License
MIT.
Copyright
© Rumkin 2017-2018