Modern and fast web framework for nodejs.
Quick Start
We recommend the Official Yo Generator for generating new applications.
# Create a folder for your project
mkdir /path/to/my/project
# Change to your project folder
cd /path/to/my/project
# Install yo and kikwit generator
npm install yo generator-kikwit -g
# Generate application
yo kikwit
# Launch
npm start
# With the above command the application will listen on port 3000 by default.
# The port can be changed in your configuration settings.
# Point your browser to http://locahost:3000/home/index
Features
- HTTP2 support
- Awesome routing
- HTTP response helpers
- Extended view engines support
- Content negotiation
- Modern middlewares support using decorators
- Connect/Express middleware support
- Seamless Websockets and Server-sent events support
- Conditional requests handling
Benchmarks
(Benchmarked on Ubuntu 16.10 LTS 64-bit, Intel® Core™ i7 CPU @ 2.40GHz × 8, 8 GiB RAM, No cluster, NodeJS v9.2.0)
Controllers
By default controller classes are located in APP_ROOT/controllers/**/*
where APP_ROOT
is the application base folder. The location can be changed using the controllersRoot
configuration property.
All controller classes must be decorated with @controller
.
import { controller } from 'kikwit';
@controller
export class Products {
...
}
Actions
Controller methods decorated with at least one HTTP method decorator are treated as actions. HTTP method decorators specifiy which HTTP request methods are valid for an action.
import { controller, get, post } from 'kikwit';
@controller
export class Products {
@get
list(context) {
...
}
@post
add(context) {
...
}
}
An action can be decorated by more than one HTTP method decorator.
import { controller, post, put } from 'kikwit';
@controller
export class Products {
@post
@put
edit(context) {
...
}
}
The @all
decorator makes the decorated action valid for any HTTP request method.
List of supported http methods
Decorator | HTTP Method |
---|---|
@all | * (Any method) |
@acl | ACL |
@baselineControl | BASELINE-CONTROL |
@checkin | CHECKIN |
@checkout | CHECKOUT |
@copy | COPY |
@del | DELETE |
@get | GET |
@head | HEAD |
@label | LABEL |
@lock | LOCK |
@merge | MERGE |
@mkactivity | MKACTIVITY |
@mkcol | MKCOL |
@mkworkspace | MKWORKSPACE |
@move | MOVE |
@options | OPTIONS |
@orderpatch | ORDERPATCH |
@patch | PATCH |
@post | POST |
@propfind | PROPFIND |
@proppatch | PROPPATCH |
@put | PUT |
@report | REPORT |
@search | SEARCH |
@trace | TRACE |
@unlock | UNLOCK |
@update | UPDATE |
@versionControl | VERSION-CONTROL |
@uncheckout | UNCHECKOUT |
Routing
Kikwit supports both explicit and implicit routing.
Explicit routing
Explicit routing is when a controller or action is tagged with a @route
decorator.
import { controller, get, route } from 'kikwit';
@route('/prods')
@controller
export class Products {
@route('/catalog')
@get
list(context) {
...
}
}
In the example above, the list
action will be accessible at /prods/catalog
.
Implicit routing
Implicit routing is when a controller or action is not tagged with a @route
decorator.
import { controller, get } from 'kikwit';
@controller
export class Products {
@get
list(context) {
...
}
}
In the example above, the list
action will be accessible at /products/list
.
Route parameters
Routes can define dynamic parts in the following format :KEY
where KEY is the key used to access the corresponding value from the request context.
import { controller } from 'kikwit';
@controller
export class Products {
@route('/show/:id')
@get
details(context) {
context.send(context.params.id);
}
}
With the route above, a GET /products/show/34
request will result in a context params's id of 34.
Route parameters can use regular expressions as constraints in the following format :KEY<REGEX>
where REGEX is the regular expression constraint.
Using the example above, if the request was GET /products/show/laptop
then the context.params.id
would be laptop. But if the action route was
@route('/show/:id<\\d+>')
instead then GET /products/show/laptop
request would not be dispatched to the details
action.
Route parameters can also be specified on the controller level route decorator.
Route tokens
Tokens can be used to dynamically reference the controller and action names so you don't have to update the route paths when they are renamed.
import { controller, get, route } from 'kikwit';
@route('/api/[controller]')
@controller
export class Products {
@route('/[action]/all')
@get
list(context) {
context.send('Products list');
}
}
The list action can be accessed at /api/products/list/all
.
Route names
Action routes can specify a route name which helps generate URLs targeting the route.
import { controller } from 'kikwit';
@controller
export class Products {
@route('/show/:id', 'productDetails')
@get
details(context) {
context.send(context.params.id);
}
}
The context's routeURL(name [, params] [, query] [, validate = true])
method can be called to generate the URL.
i.e. a link to the details action's route can be generated using the following
context.routeURL('productDetails', { id: 34 })
The above would generate the string /products/show/34.
Query strings can be added to the generated URL with the help of the third argument of the context's routeURL(...)
method.
context.routeURL('productDetails', { id: 34 }, { offset: 10, pageSize: 20})
The above would generate the string /products/show/34?offset=10&pageSize=20.
The validate argument validates params values against route constraints (if any). Passing false
skips any validation.
Request Data
Query strings
Query strings values can be accessed using the query
property of the request context.
import { controller, get } from 'kikwit';
@controller
export class Page {
@get
echo(context) {
const offset = context.query.offset;
const limit = Math.min(context.query.limit, 50);
const categories = context.query.categories;
context.sendJSON({ offset, limit, categories });
}
}
A GET /page/echo?offset=15&limit=20&categories=laptop&categories=phablet
request to the echo
action above will return the following JSON document
{ offset: 15, limit: 20, categories: ['laptop', 'phablet'] }
By default the querystring package's parse
function is used to parse the query string.
You can assign a global custom function for query parsing by setting the queryParser
configuration property.
This function should accept a string and return an object which will accessible via the Context's query
property:
E.g.
{
...,
queryParser: querystring.parse,
...
}
When the queryParser
configuration property is set to false
the query string is not parsed at all and the query
property is not set.
You also can also assign a custom parser to a specific controller or action only using the @queryParser
decorator.
import { controller, get, queryParser } from 'kikwit';
@controller
export class Page {
@queryParser(myParser)
@get
echo(context) {
const offset = context.query.offset;
const limit = Math.min(context.query.limit, 50);
const categories = context.query.categories;
context.sendJSON({ offset, limit, categories });
}
}
function myParser(str) {
let [offset, limit, categories] = str.split('-');
categories = categories.split(',');
return { offset, limit, categories };
}
A GET /page/echo?15-20-laptop,phablet
request
to the echo
action will return the following
{ offset: 15, limit: 20, categories: ['laptop', 'phablet'] }
Request body
The body of the request can be accessed using the body
property of the request context.
import { controller, post } from 'kikwit';
@controller
export class User {
@post
register(context) {
const username = context.body.username;
const gender = context.body.gender;
context.sendJSON({ username, gender });
}
}
The following request
curl -X POST -F 'username=Shaka Zulu' -F 'gender=M' http://domain.tld/user/register
will return
{ username: 'Shaka Zulu', gender: 'M' }
Request Content Type | Default parser |
---|---|
urlencoded | Busboy |
multipart | Busboy |
Text | None. Context.body is a string |
JSON | JSON.parse |
All others | None. Context.body is a Buffer |
The body parser can be changed by setting a custom bodyParser
configuration property.
This function should accept a request Context
object and return a promise that resolves to an object
containing two properties: body
and files
.
The files
property should represent the uploaded files is any.
E.g.
import formidable from 'formidable';
{
...,
bodyParser: myParser,
...
}
function myParser(context) {
return new Promise((resolve, reject) => {
var form = new formidable.IncomingForm();
form.parse(context.request, function(err, fields, files) {
if (err) {
return reject(err);
}
return resolve({ body: fields, files });
});
}
}
When the bodyParser
configuration property is set to false
the body is not parsed at all and the body
property is not set.
You also can also assign a custom parser to a specific controller or action only using the @bodyParser
decorator.
import { controller, post, bodyParser } from 'kikwit';
@controller
export class User {
@bodyParser(myParser)
@post
register(context) {
...
}
}
Context object
Controller actions and all interceptors accept a single request context argument which provides the following properties and methods:
-
config
The application configuration properties.
-
host
The request HOST header, or
X-FORWARDED-HOST
request header value (if present) whentrustProxy
setting is set totrue
, plus the port number. -
hostname
The request HOST header, or
X-FORWARDED-HOST
request header value (if present) whentrustProxy
setting is set totrue
. -
ip
The request client ip address, or the first entry from
X-FORWARDED-FOR
request header value (if present) whentrustProxy
setting is set totrue
. -
ips
An array containing the request client ip address, or the all entries from
X-FORWARDED-FOR
request header value (if present) whentrustProxy
setting is set totrue
. -
state
The state holds request data that are available to interceptors, action and views. Values stored in the
state
property are specific to the current request only. -
port
The request port number, or
X-FORWARDED-PORT
request header value (if present) whentrustProxy
setting is set totrue
. -
protocol
The request protocol (http or https), or
X-FORWARDED-PROTO
request header value (if present) whentrustProxy
setting is set totrue
. -
statusCode
Gets or sets the response status code.
-
subdomains
An array containing the request subdomains. By default the domain is the last two parts of the host. The
subdomainOffset
configuration property can be used to specify the number of parts that constitutes the application domain. The remaining parts are the subdomains.e.g. for
user.api.kikwitjs.com
the subdomain would be ['api', 'user'] ifsubdomainOffset
configuration property is 2 (default) and ['user'] if it's set to 3. -
statusMessage
Sets the response status message.
-
download(path [, filename], [contentType] [, options])
Sends the contents of the file at path to for download.
The base name of the path argument is used as default value for the
Content-Disposition
headerfilename
value unlessfilename
argument is specified.Content-Disposition
header is set toattachment
unless already set by the calling code.The response content type is derived from the file extension but an explicit value can be specified using the
contentType
argument.The optional
options
argument can specify the following:-
lastModified: if
true
, set theLast-Modified
header to the file's last modified time. -
root: the root folder containing
path
.If
root
is provided thenpath
is always treated as relative.If
root
is not provided andpath
is not absolute thenroot
defaults to the application root folder. -
headers: additional headers to add to the response.
If
headers
is an object then its keys are used as header names and the corresponding values as header values.If
headers
is a function then it's called with a single argument representing the full path of the file and any returned object is used to generate additional headers.
-
-
next()
Calls the next interceptor if any. When called in the last
@before
interceptor, this method will call the target action. If called in the last@after
interceptor, the call does nothing.The
next()
valid to call in interceptors only. -
noEvent(interval)
Signals that there is no server-sent event data to sends to the client.
The interval argument specifies the time interval to wait until the next call to generate another event.
-
push(filePath [, contentType])
Pushes a file to the client. Only works when HTTP/2 is enabled.
When
contentType
argument is not specified orundefined
, a value is derived from thefilePath
extension, if any.text/plain
is used when no valid match exists.Please note that the file is actually pushed only if when calls to push are followed by a call to
context.render(...)
. -
redirect(url [, statusCode])
Redirects the request by setting a
LOCATION
header.The
statusCode
argument defaults to 302. -
redirectToRoute(name, params, query = null, validate = true, statusCode = null)
-
removeHeader(name)
Removes a response header.
-
render([viewPath] [, locals] [, contentType])
Renders a view template.
The optional
locals
argument are provided as the view model. IfviewPath
is not provided the view is looked up based on the controller and action name. The default is to look respectively in the follwing folders:- APP_ROOT/views/[CONTROLER_NAME]/[ACTION_NAME]
- APP_ROOT/views/[CONTROLER_NAME]
- APP_ROOT/views.
The
views.root
configuration property defines the root folder for views and defaults to APP_ROOT/views where APP_ROOT is the application's root folder. -
routeURL(name [, params] [, query] [, validate = true])
Generates a url based on a route
name
.The
params
header argument specifies any route parameter values.query
represents an object whose property names are added as the query string keys and corresponding values as query string values.The
validate
argument indicates whether the route parameters are checked against related patterns if any. -
send(body [, contentType])
Sends the response using
body
, converted to string, as content.The
contentType
argument, if not provided, defaults totext/plain; charset=utf-8
. -
sendEvent(eventDetails, interval)
Sends a server-sent event.
The interval argument specifies the time interval to wait until the next call to generate another event. If there is no data to send then the Context's
noEvent(interval)
method should be called.The eventDetails argument should have the following properties:
-
event
The event name. If this property is specified, an event will be dispatched on the browser to the listener for the specified event name; This property corresponds to the Server-sent events standard's event field. The EventSource's
onmessage
handler is called if no event name is specified for a message. -
data
The data field for the message. When the caller's EventSource receives multiple consecutive lines that begin with
data:
, it will concatenate them, inserting a newline character between each one. Trailing newlines are removed.This field corresponds to the Server-sent events standard's data field.
-
id
The event ID to set the caller's EventSource object's last event ID value. This field corresponds to the Server-sent events standard's id field.
-
retry
The reconnection time to use when attempting to send the event. This must be an integer, specifying the reconnection time in milliseconds. This field corresponds to the Server-sent events standard's retry field.
-
-
sendJSON(body)
Sends a JSON response using
body
as content.The
CONTENT-TYPE
header, if not set by the calling code, is set toapplication/json
. -
sendJSONP(body)
Sends a JSON response with JSONP support.
The default JSONP callback name is callback and can be changed using
json.callbackParam
configuration property. -
sendFile(path, contentType, options)
Pipes the contents of the file at path to the response stream.
The response content type is derived from the file extension but an explicit value can be specified using the
contentType
argument.The optional
options
argument can specify the following:-
lastModified: if
true
, set theLast-Modified
header to the file's last modified time. -
root: the root folder containing
path
.If
root
is provided thenpath
is always treated as relative.If
root
is not provided andpath
is not absolute thenroot
defaults to the application root folder. -
headers: additional headers to add to the response.
If
headers
is an object then its keys are used as header names and the corresponding values as header values.If
headers
is a function then it's called with a single argument representing the full path of the file and any returned object is used to generate additional headers.
-
-
sendStatus(code, message)
-
setHeader(name, value)
Sets a response header.
-
setHeaders(headers = {})
Sets response headers.
The
headers
argument can be aMap
or an plain object. -
skipToAction()
Skips any remaining
@before
interceptors, if any, and calls the target action.The
skipToAction()
valid to call in@before
interceptors only. -
stream(stream, contentType)
Pipes
stream
to the response steam.The
contentType
defaults toapplication/octet-stream
-- throw(err)
Sends a response with a 500 status code and status text set to err.message
.
When the environement is development
the error stack trace is also included.
You can set the environment using NODE_ENV
environment variable, e.g. NODE_ENV=development
or NODE_ENV=production
.
Interceptors
Interceptors are middlere wafunctions that are configured to run before or after an action. They can be used for logging, authorization, etc...
Kikwit supports defining interceptors using decorators on the controller or action levels. Controller interceptors apply to all controller actions. Action interceptors apply to the decorated action only.
Interceptors have the same signature as controller actions, they accept a single Context argument.
Before interceptors
Before interceptors are specified using the @before(...interceptors)
decorator.
'use strict';
import { before, controller, get } from 'kikwit';
@before(authenticate)
@controller
export class Home {
@get
index(context) {
context.sendJSON(context.locals);
}
@before(authorize)
@get
details(context) {
context.sendJSON(context.locals);
}
@before(authorize)
@get
info(context) {
context.sendJSON(context.locals);
}
@before(greet, authorize)
@get
hello(context) {
context.sendJSON(context.locals);
}
}
function authenticate(context) {
context.locals.userId = Math.trunc(Math.random() * 1000000);
context.next();
}
function authorize(context) {
context.locals.authorized = (context.locals.userId % 2 == 0);
if (!context.locals.authorized) {
return context.sendStatus(403, JSON.stringify(context.locals));
}
context.next();
}
function greet(context) {
context.locals.greeted = true;
return context.skipToAction();
}
After interceptors
After interceptors are specified using the @after(...interceptors)
decorator.
import { after, get, inject } from 'kikwit';
@after(addRandomHeader)
export class Products {
@inject('myService')
@get
list(context) {
context.services.myService.getProducts().then(products => {
// HTTP response will contain an 'X-RANDOM-NUMBER' header
context.sendJSON(products);
});
}
@get
hello(context) {
// HTTP response will contain an 'X-RANDOM-NUMBER' header
context.send('Hello');
}
}
function addRandomHeader(context) {
context.setHeader('X-RANDOM-NUMBER', Math.random().toString());
context.next();
}
Connect/Express middleware support
Connect/Express middlewares are supported via the use
helper function.
The use
function transforms a middleware to a before interceptor.
import { before, controller, get, use } from 'kikwit';
@before(use(requestStamp))
@controller
export class Products {
@get
list(context) {
// context.request.stamp is set
context.send('List');
}
@get
details(context) {
// context.request.stamp is set
context.send('Details');
}
}
function requestStamp(req, res, next) {
req.stamp = Date.now();
next();
};
Please always use the Context
helper methods when possible and avoid accessing the underlying request and response objects directly.
Services
By default service classes are located in APP_ROOT/services/**/*
where APP_ROOT
is the application base folder. The location can be changed using the servicesRoot
configuration property.
All service classes must be decorated with the @service
decorator.
The @service
requires a string argument which defines the key to use when injecting the service using the @inject(...KEYS)
decorator.
By default each request gets its own instance of the injected service.
import { service } from 'kikwit';
@service('adder')
export class Adder {
add(a, b) {
return a + b;
}
}
import { controller } from 'kikwit';
@inject('adder')
@controller
export class Arithm {
sum(context) {
let [a, b] = [7, 11];
let sum = context.services.adder.add(a, b); // context.services.adder from @inject('adder')
return context.sendJSON({ a, b, sum });
}
}
It is possible to get a service injected as a singleton by prefixing the key passed to the @inject(...KEYS)
decorator with @
.
import { service } from 'kikwit';
@service('adder')
export class Adder {
add(a, b) {
return a + b;
}
}
import { controller } from 'kikwit';
@inject('@adder') // '@adder' instead of 'adder'
@controller
export class Arithm {
sum(context) {
let [a, b] = [7, 11];
let sum = context.services.adder.add(a, b); // context.services.adder from @inject('@adder')
return context.sendJSON({ a, b, sum });
}
}
It is also possible to get the same instance of a service injected into an action across multiple requests by prefixing the key passed to the @inject(...KEYS)
decorator with @@
.
import { service } from 'kikwit';
@service('adder')
export class Adder {
add(a, b) {
return a + b;
}
}
import { controller } from 'kikwit';
@controller
export class Arithm {
@inject('@@adder') // '@@adder' instead of 'adder'
sum(context) {
let [a, b] = [7, 11];
let sum = context.services.adder.add(a, b); // context.services.adder from @inject('@@adder')
return context.sendJSON({ a, b, sum });
}
}
In the example above, the instance injected into the sum
action will only be reused by request to the sum
action only.
To restrict a service to always be injected as a singleton, please pass true
as a second argument to @service([KEY], [SINGLETON])
decorator.
import { service } from 'kikwit';
@service('adder', true)
export class Adder {
add(a, b) {
return a + b;
}
}
In the example above Adder
will always get injected as a singleton regardless of how the format of the key used at the injection point.
Services injected at controller level are available to all of controller's actions.
Routes and services overview page
An overview of an application's routes and services can be accessed at /_debug/overview
url path when running the application
with the development environment (default).
The overview page is not available with other environments.
Cookies
Kikwit supports cookies via the cookies package which optionally provides cookie signing, to prevent tampering, using the keygrip package.
import { controller, get } from 'kikwit';
@controller
export class Products {
@get
index(context) {
context.cookies.set('unsigned', '12345');
context.cookies.set('signed', '67890', { signed: true });
context.redirect('/products/details');
}
@get
details(context) {
var unsigned = context.cookies.get('unsigned');
var signed = context.cookies.get('signed', { signed: true });
context.sendJSON({ unsigned, signed });
}
}
The keys used to sign the cookies can be set in the config file using the cookieParser.keys
entry as follows:
{
...
cookieParser: {
keys: ['~strong-key-01!', '#strong-key-02?']
}
...
}
Cookie parsing can be disabled by setting the cookieParser
to a falsy value.
{
...
cookieParser: null
...
}
Please refer to the cookies package for more info regarding additional features for cookies.
Error handling
An error handler can be specified using the @onError
decorator. The error that was raised is accessible via the Context.error
property.
import { controller, get, onError } from 'kikwit';
@onError(errorHandler)
@controller
export class Products {
@get
list(context) {
const nothing = null;
context.send(nothing.toString());
}
@get
details(context) {
const a = null;
const result = a.startsWith('b');
context.send('Unreachable');
}
}
// The following handler will be called when an exception in raised in list or details actions
function errorHandler(context) {
// log context.error
context.render('niceErrorPage', context.error);
}
WebSockets support
Annotating a controller with @webSocket
decorator turns it into a WebSocket handler.
Controller class methods are automatically used as WebSocket events listeners when
they are annotated with the following decorators:
Method | Handled WebSocket event | Context body (context.body ) |
|
---|---|---|---|
@onClose |
close |
{code, message} |
Called when the connection is closed. code is defined in the WebSocket specification. |
@onConnect |
connection |
undefined |
Called when the connection is established. |
@onMessage |
message |
{data, flags} |
Called when data is received. flags is an object with member binary. |
@onPing |
ping |
{data, flags} |
Called when a ping is received. flags is an object with member binary. |
@onPong |
pong |
{data, flags} |
Called when a pong is received. flags is an object with member binary. |
import { controller, webSocket, onConnect, onMessage, onClose } from 'kikwit';
@webSocket
@controller
export class Forum {
@onConnect
join(context) {
context.send(`Welcome ${context.query.username}!`);
}
@onMessage
receive(context) {
context.send(`You said: ${context.body.data}`);
}
@onClose
gone(context) {
console.log(`${context.query.username} has gone`);
}
}
You can use a script similar to the following to call the above WebSocket controller from a web browser
var ws = new WebSocket('ws://HOST[:PORT]/forum?username=mega-mind');
ws.onmessage = function(event) {
console.log(event.data);
};
ws.send("I'm bored!");
The following Context methods are available on WebSocket controllers
Context method | Description |
---|---|
close([code], [data]) |
Gracefully closes the connection, after sending a description message. |
pause() |
Pause the client stream. |
ping([data], [options], [dontFailWhenClosed]) |
Sends a ping. data is sent, options is an object with members mask and binary . dontFailWhenClosed indicates whether or not to throw if the connection isn't open. |
pong([data], [options], [dontFailWhenClosed]) |
Sends a pong. data is sent, options is an object with members mask and binary . dontFailWhenClosed indicates whether or not to throw if the connection isn't open. |
resume() |
Resume the client stream |
send(data, [options], [callback]) |
Sends data through the connection. options can be an object with members mask , binary and compress . The optional callback is executed after the send completes. |
stream([options], callback) |
Streams data through calls to a user supplied function. options can be an object with members mask and binary . callback , of the form function (error, send) , is executed on successive ticks of which send is function (data, final) . |
terminate() |
Immediately shuts down the connection. |
Behind the scenes, Kikwit uses the ws package.
Server-sent Events support
Sending a server-sent is as simple as calling context.sendEvent(eventDetails, interval)
.
Please see the Context object for more details about the sendEvent
method.
import { controller, get } from 'kikwit';
@controller
export class StockMarket {
@get
ticker(context) {
let lastEventId = context.lastEventId || 0;
const randomValue = () => Number.parseFloat((Math.random()*2).toFixed(2)) * (Math.random() < 0.5 ? -1 : 1);
const eventDetails = {
data: {
DJIA: randomValue(),
Nasdaq: randomValue(),
['S&P 500']: randomValue(),
GOLD: randomValue()
},
id: ++lastEventId,
event: 'tick',
retry: 5000
};
context.sendEvent(eventDetails, 5000);
}
}
The client call for the above would be something like
var eventSrc = new EventSource('/stockmarket/ticker');
eventSrc.onmessage = function(event) {
console.log(event.data);
}
Static Files
Any files located under APP_ROOT
/public are available to any user under the /public/
path.
The directory for static files can be changed by setting the staticFiles.root
configuration key.
For example, to set the static files folder to be served from a folder called assets you can use the following configuration.
The configuration below exposes static files under /assets/*
.
{
staticFiles: {
root: 'assets'
}
}
Directory browsing
Directory browsing allows users of your web app to see a list of directories and files within the static files folder.
Directory browsing is disabled by default. To enable directory browsing, set the staticFiles.directoryBrowsing
configuration key. Setting it to true
enables directory browsing for all static files directories.
{
staticFiles: {
directoryBrowsing: true
}
}
Secondly, directoryBrowsing
can be set to an array of paths of the only folders to expose.
In this case the paths are relative to and must be located under the staticFiles.root
folder.
Any leading or trailing slashes are ignored.
{
staticFiles: {
directoryBrowsing: [ 'images/', 'scripts', '/styles' ]
}
}
For more control, directoryBrowsing
can be set to a predicate function that accepts
a folder path and returns true to allow browsing, or false to disable it.
The folder path is relative to the static folder path setting.
In the example below, only requested folders with names starting with _
will be browsable.
{
staticFiles: {
directoryBrowsing: (folder) => folder.startsWith('_')
}
}
When directoryBrowsing
is enabled for a folder, you can set an indexFile
file to display when the user requests the directory.
{
staticFiles: {
directoryBrowsing: true,
indexFile: 'index.html'
}
}
When the index file is not set, or when it cannot be found in the requested directory,
an auto-generated file is used instead.
Custom HTTP headers
Custom headers can be added to static files responses using the staticFiles.headers
configuration key.
To add headers using an object use the following:
{
staticFiles: {
directoryBrowsing: true,
headers: {
'SOME-HEADER': 'Value',
'OTHER-HEADER': 'Other Value',
},
},
}
Alternatively, a function can be added for more flexibility:
{
staticFiles: {
directoryBrowsing: true,
headers: (file) => {
// the [file] argument is the requested file path relative to the static files root.
// Compute headers here, for example based of the type of [file].
const hdrs = {
'SOME-HEADER': 'Value',
'OTHER-HEADER': 'Other Value',
};
return hdrs;
},
},
}
HTTP Port
The default http port number is 3000
.
To select a different port number update the config as follows
// config/default.js
import fs from 'fs';
export default {
...
http: {
port: 8080,
},
...
}
If you want to disable HTTP, because you're using HTTPS only, set the http
setting to false
.
// config/default.js
import fs from 'fs';
export default {
...
http: false
...
}
HTTPS Support
To run the server over HTTPS, you need to set the https
configuration property.
Internally, the https
options are passed as first argument to node's https.createServer(...)
function.
More details can be found here.
The example below assumes you have server.key private key file and server.crt publlic certificate file in the root of your project.
// config/default.js
import fs from 'fs';
export default {
...
https: {
port: 3001, // default if not specified
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt'),
// pfx: fs.readFileSync('server.pfx'),
},
...
}
HTTP/2 Support
HTTP/2 is disabled by default. To enable it, you need to set the http2
configuration property.
When set to true
, HTTP/2 is supported with fallback to HTTP/1 for clients that don't support it.
Some major browsers support HPP/2 only when used with HTTPS, so HTTP should be explicitely disabled.
// config/default.js
import fs from 'fs';
export default {
...
http: false, // disables http
https: {
port: 3001,
cert: fs.readFileSync('./certificates/pub.cert'),
key: fs.readFileSync('./certificates/priv.key')
},
http2: true
...
}
To support HTTP/2 only, please configure as follows
// config/default.js
import fs from 'fs';
export default {
...
http: false, // disables http
https: {
port: 3001,
cert: fs.readFileSync('./certificates/pub.cert'),
key: fs.readFileSync('./certificates/priv.key')
},
http2: {
allowHTTP1: false
}
...
}
Once HTTP/2 enabled, files can be pushed to clients as below. Files to push must be under the static files root folder.
'use strict';
import { controller, get } from 'kikwit';
@controller
export class Home {
@get
index(context) {
context.push('css/default.css', 'text/css');
context.push('js/main.js', 'application/javascript');
context.render({ message: 'Hello' });
}
}
Using the cluster module
Your application can take advantage of multi-core systems and run more than one process (using node's cluster
module).
The configuration cluster
setting allows you to set the number of processes you want to run.
When the cluster
option is set to true
then the number of processes will match the number of cores on your server.
You can also directly specify the number of processes you want.
If that number is less than 1, or if you specify false
, then you application won't run in a cluster and will just have a single process.
If that number is greater than the number of cores then the actual number of cores will be used instead.
// config/production.js
export default {
...
cluster: true, // Use all cores
// cluster: 3, // Only use three cores
...
}
Configuration
Configuration is highly flexible and settings can be read from different sources like JSON, environment variables, service class files, etc.
// app.js file
'use strict';
import { Server } from 'kikwit';
const server = new Server();
server.configure(config => {
// Merge settings from the specified json file.
// You can pass a second argument to indicate whether an exception
// should NOT be thrown if the file is missing.
config.addJsonFile('config/default.json'); // Fails if the file is missing
config.addJsonFile(`config/${config.environement}.json`, true); // Doesn't fail if the file is missing
// Merge settings from the specified javascript file.
// You can pass a second argument to indicate whether an exception
// should NOT be thrown if the file is missing.
config.addJavaScriptFile('config/default.js');
config.addJavaScriptFile(`config/${config.environment}.js`, true);
if (config.isEnvironment('development')) {
// Merge user configuration settings.
// More details on services can be found at https://github.com/kikwit/kikwit#user-configuration-tool
config.addUserConfig();
}
// Merge environment variables. A string argument (prefix) can be passed to add only the variables
// that start with the specified prefix. The prefix is removed from the variable name when
// the variable is added to the configuration.
// Use double underscores (__) to create a hierarchy.
// E.g. 'DB__HOST:ALPHA' gets added as { DB: { HOST: 'ALPHA' } }
// config.addEnvironmentVariables();
config.addEnvironmentVariables('APP_');
// Merge the specified object
config.merge({
randomFlag: true,
log: {
level: 'debug',
dest: 'file'
}
});
// Read config entries with `config.get(KEY)`
config.get('log.dest') == 'file';
});
server.start().then(() => {
console.log(`Server started`);
});
To avoid storing sensitive data in your code and source control, you can pass them to your program by the use of environment variables or, when in development, by using the User Configuration tool.
User Configuration tool
The User configuration tool is used to managed user specific configuration settings. This tool is included in the official Kikwit yo generator.
You need to run this too in your project root folder.
To read all settings:
yo kikwit:user-config
To set a setting:
yo kikwit:user-config set db.Host GAMMA
yo kikwit:user-config set db.Port 8885
To read a setting:
yo kikwit:user-config get db
yo kikwit:user-config get db.Port
To remove a setting:
yo kikwit:user-config remove db.Port
To clear all settings:
yo kikwit:user-config clear
Use the following to add user configuration settings in your application:
// app.js file
'use strict';
import { Server } from 'kikwit';
const server = new Server();
server.configure(config => {
// ...
if (config.isEnvironment('development')) {
// Merge user configuration settings.
// More details on services can be found at https://github.com/kikwit/kikwit#user-configuration-tool
config.addUserConfig();
}
// ...
});
server.start().then(() => {
console.log(`Server started`);
});
Prerequisites
- Node.js >= 9.2
Tests
npm test
Maintainers
- Elondo Mbonze