kickapp

Application architecture framework with pluggable services

npm install kickapp
43 downloads in the last week
157 downloads in the last month

Version Dependency Status Build Status

KickApp

KickApp is an application architecture framework for organizing the logic into runnable services.

Split your logical components into services contained in a top-level Applicaton object, define service dependencies and launch them all with a single app.start()!

Key features:

  • Structured application architecture approach
  • Service workflow: init(), start(), stop()
  • Service dependencies resolution
  • Can deal with hierarchical services
  • Promise-based: using the q package
  • Unit-tested

Table of Contents

Core Components

Service

A Service is an arbitrary constructor function or an object that implements the IService interface:

  • Its constructor function gets an Application as the first argument. More arguments can be provided with the Application.addService() method.
  • It can optionally have the init() method: here you can perform some preparation steps
  • It must have the start() and stop() methods which bring your service up and down respectively.

Consider an example:

/** Database service
 * @param {Application} app
 *     The parent Application
 * @param {String} url
 *     Connection string
 * @constructor
 * @implements {IService}
 */
var DbService = function(app, url){
    this.app = app;
    this.url = url;
    this.client = undefined;
};

DbService.prototype.init = function(){
    this.client = new DatabaseClient(this.url); // init some imaginary client
};

DbService.prototype.start = function(){
    return this.client.connect(); // assuming it's promise-based
};

DbService.prototype.stop = function(){
    return this.client.disconnect();
};

Having such a service, you can:

  • Add it to an Application
  • Define service dependencies
  • Launch them all in the correct order

Object Service

If your service is simple, you don't have to implement the constructor and everything. Use an object instead, which is also a valid service:

var DbService = {
    app: undefined,
    client: new DatabaseClient('db://localhost/'),
    start: function(){
        return this.client.connect();
    },
    stop: function(){
        return this.client.disconnect();
    },
};

It will automatically get the app property when added to the Application.

Application

An Application is the container for your services.

You define an application by creating an instance of it, providing a constructor function as an argument:

var kickapp = require('kickapp');

var App = new kickapp.Application(function(configFile){
    this.config = require(configFile); // init the configuraton

}, 'app/config.js');

By design, an Application does not have custom start/stop behavior: instead, it wraps services.

Application(App, ...args)

Structure

Application.addService(name, serviceConstructor, ...args):Application

Add a service to the application.

  • name: String: Name of the service. Use any reasonable string.
  • serviceConstructor: Function: Constructor function for an object that implements the IService interface.
  • ...args: Variadic arguments for the service constructor

Returns: an instance of ServiceWrapper (see below).

This function can also be called inside the Application constructor:

var kickapp = require('kickapp');

var App = new kickapp.Application(function(configFile){
    // Load the configuration
    this.config = require(configFile);

    // Add services
    this.addService('db', require('./services/db.js'), this.config.db );

    this.addService('web', require('./services/web.js'), this.config.web )
        .dependsOn('db'); // Define a dependency on other services

}, 'app/config.js');

Application.dependsOn(name, ...names):Application

Add dependencies for the recently added Service.

When a Service depends on other services, they will be started before starting this one.

Dependencies can be given either as arguments, or as a single array argument.

Application.get(serviceName):IService

Get the Service object by name:

app.get('db').client; // get the Service property

Application.getServiceWrapper(serviceName):ServiceWrapper

Get the ServiceWrapper object by name.

See: ServiceWrapper

Application.getServiceNames():Array.

Get the list of service names added to this Application object.

Application.isRunning():Boolean

Check whether the Application is running.

An Application is running if all its services are running.

Workflow

Service methods are run in the following fashion:

  • Service constructors are called immediately whilst you add your services to the Application
  • init(), start(), stop() methods are called when the corresponding Application method is called. Unlike the constructor, these honor the service dependencies.

Application.init()

Call init() on all services, honoring the dependencies. Returns a promise.

Note that IService.init is optional.

Application.start()

Call start() on all services, honoring the dependencies. Returns a promise.

If some services were not yet initialized with init(), Application does that.

Application.stop()

Call stop() on all services, honoring the dependencies in reverse order. Returns a promise.

Events

Application is an EventEmitter which fires the following events:

  • 'init': All services have been initialized
  • 'start': All services have been started
  • 'stop': All services have been stopped

ServiceWrapper

Each Service is internally wrapped in ServiceWrapper which controls the service state.

  • ServiceWrapper is an EventEmitter which fires the 'init', 'start', 'stop' events of the service
  • Contains metainformation about the Service: service, initialized, running, dependencies

Usually, you won't need it. See the source code: ServiceWrapper.

Full Example

var kickapp = require('kickapp');

// Application
var App = new kickapp.Application(function(configFile){ // Application constructor
    // Load the configuration
    this.config = require(configFile);

    // Services
    this.addService(
        'db', // Service name
        require('./services/db.js'), // Service constructor with init()/start()/stop() methods
        this.config.db // Arguments for the Service constructor
    );

    this.addService('web', require('./services/web.js'), this.config.web )
        .dependsOn('db'); // Dependencies on other services

}, 'app/config.js'); // Arguments for the Application constructor

// Launch it
App.start()
    .then(function(){
        console.log('Application initialized and started!');
    })
    .catch(function(err){
        console.error('Application.start failed:', err.stack);
    });

// Stop the services properly when the application exitsq
process.on('SIGINT', process.exit);
process.on('exit', function(){
    App.stop()
        .catch(function(err){
            console.error('Application.stop failed:', err.stack);
        });
});

Promise-Haters

If you dislike promises, you can always get back to the old good NodeJS-style callbacks:

var App = new kickapp.Application(function(){
});

App.start().nodeify(function(err){
    if (err)
        console.error('Application.start failed:', err.stack);
    else
        console.log('Application initialized and started!');
});

Application as a Service

The Application object can be used as a service as it implements the IService interface. This allows creating reusable components from sustainable application parts and add them to other applications as a service:

var app = new kickapp.Application(function(){
    // ... init some services
});

var top = new kickapp.Application(function(){
    this.addService('app', app); // added as a service
});

Bundled Services

KickApp comes with some handy bundled services.

NetService

NetService is a helper to wrap net.Server, http.Server, https.Server, tls.Server, dgram.createSocket network servers in a KickApp service. It also supports static configuration with generalized interface.

NetService accepts two arguments:

  • config: Object: Server configuration object:

    • config.lib: String: The server type to create. Supported values: 'net' (TCP socket), 'tls' (TLS socket), 'http' (HTTP server), 'https' (HTTPS server), 'udp4' (UDP IPv4 socket), 'udp6' (UDP IPv6 socket).
    • config.listen: Array: Array of arguments for the listen() function.

      For 'net', 'http', 'https': port, [hostname], [backlog]

      For 'tls': port, [host]

      For 'udp4', 'udp6': port, [address]

    • config.options: Object: Options for the createServer() function. See NodeJS Manual.

      Note: for 'tls' and 'https' certificates & stuff, you can optionally specify filenames for the following keys: 'pfx', 'key', 'cert', 'ca', 'crl'.

  • accept: Function: Method used to accept the incoming connections.

      For `'net', 'tls'`: `function(sock: net.Socket)`
    
      For `'http'`, `'https'`: `function(req: http.ClientRequest, res: http.ServerResponse)`
    
      For `'udp4'`, `'udp6'`: `function(msg: String|Buffer, rinfo: Object)`
    

Wielding the service, you can start/stop it an get error handling:

var app = new kickapp.Application(function(){
    this.addService('net', kickapp.services.NetService,
        { lib: 'net', listen: [6001, 'localhost'] },
        function(sock){
            sock.end('hi');
        }
    );
    this.addService('http', kickapp.services.NetService,
        { lib: 'http', listen: [6080, 'localhost'] },
        function(req, res){
            res.end('hi');
        }
    );
});

NetService has the following properties:

  • config: Server configuration object
  • server: The created server
npm loves you