Genji build status

Writing reusable, modular and flexible node.js applications made easy.

Introduction

Genji is not a full stack web framework, instead it focuses on making your code reusable and flexible enough to integrate with other components and frameworks. In development of modern web application, your service usually consumed by various kinds of clients with different manners. You may have web site for browser user, private apis for mobile clients, public apis for third-party developers and internal apis for queues or job workers. Genji helps you write reusable code by providing extensible plugin/middleware system, routing/transport agnostic application class, models fields validation and custom getter/setter method, views layout manager and namespaced template overriding, url routing with hook support. While it doesn't mean you have to stick with a particular development style or technology, genji is highly modular designed and customizable, so you can decide which part of the framework to use and how.

About the name "Genji"

The word Gen Ji in Chinese means root base and it's also the name of the Japanese era name which was created to mark the beginning of a new 60-year cycle of the Chinese zodiac. So, basically it means something fundamental that you can grow with. The philosophy of this project follows this meaning.

License

(The MIT License)

Copyright (c) 2010-2013 Senmiao Liu senmiao.liu@gmail.com

The way to Genji

The simplest way to use Genji is to use it as a url based http request router

var genji = require('genji');
var http = require('http');

// create a router instance
var simpleRouter = genji.route();

// routing request to function by matching url
simpleRouter.get('^/$', function(context) {
  context.send('Hello world!');
});

// create a http server object
var server = http.createServer();

// listen to request event
simpleRouter.listen(server);

// start handling request
server.listen(8888, '127.0.0.1');

This is regular request routing functionnality which you can find in a lots of frameworks of any language. Of course, this is not what all Genji can do. If you feel the above example already meets most of your needs, then go to Router to see detailed usages. And also go Context check out what it is and see how you can use and extend it. If it's not, and you feel this is too primitive. You have another option called Site which is more suitable for large/complex project. So you can change the way of using Genji during evolvement of your project and Genji helps you grow smoothly during that process.

Router

Router routes http request to designated handling function based on the requesting url. It supports calling hook function before/after dispatch to handling function. Router can be used standalone which means no site/middleware/app involved.

Add route definition

  var genji = require('genji');

  var router = genji.route({urlRoot: '/home'});

  // handle GET request for url '/home/hello?title=Mr' (urlRoot + url)
  router.get('/hello/(.*)', function(context, name) {
    context.sendHTML('Hello, ' + context.query.title + ' ' + name);
  });

  // 'urlRoot` will not be prefixed before your url if it starts with '^'
  router.post('^/post', function(context) {
    // post parameters will be parsed if context listen to the 'params' event.
    context.on('params', function(params) {
      // do something
      context.send('ok');
    });
  });

Hooks

You can use Hooks to do some tasks before and after dispatch. Hook functions together with handle function will be chained into an array by router. And all functions in chain will be called in order during dispatch. During the dispatching process, you must explicitly call next or return true in hook function to call the next function in chain.

We use null to mark the position of the dispatch function. During dispatch, the null placeholder will be replaced by handle function. Some special cases when: - null is at beginning of the array, all functions are post hook (e.g. [null, fn1, fn2]) - null is not presented or at the end of array, all functions are pre hook (e.g. [fn1, fn2] === [fn1, fn2, null]) - the hook is a function, it means the function is a pre hook (e.g. fn1 === [fn1])

  function preHook(context, next) {
    // you can call `next` asynchronously, the next function in chain won't be called
    // until you call `next`
    setTimeout(next, 1000);
  }

  function postHook(context, next) {
    // if you return true, the next function in chain (if any) will be called immediately
    return true;
  }

  router.get('/hooked', function() {}, [preHook, null, postHook]);

Listen to server event

You can use router directly with HttpServer instance.


  // listen to 'request' event of HttpServer instance
  router.listen(server);

If you have more complex project, please use Site with App.

API

Router exposed by require('genji').Router. Instance can be created by


    var router = require('genji').route(options);

genji.route(options:Object)

Takes optional options and create a new genji.Router instance.

Supported options:

Methods

{get|post|put|delete|head}(url:{String|RegExp}, handler:Function, /*optional*/ options:Object)

The get/post/put/delete/head route defining methods of Router instance have the same signature:

Routing rules are grouped by http method. Previous rule could be overriden by subsequently calling routing method with same http method and url. That means you can have different handling functions for the same url with different http method.

mount(urls:{Array})

Add batch of routing rules at once. Each element in the urls array should be an array with members of following order:

notFound(url:{String|RegExp}, handler:Function)

The miss matched matcher. You can use it to handle miss matched requests based on different url.


  router.notFound('^/blog/*', function(context) {
    // for miss matched url start with `/blog/`
  });

  router.notFound('^/*', function(context) {
    // for any other cases
  });

Router#route(type:String, url:String, context:Object, /*optional*/ notFound:Function):Boolean

The routing function, it takes input and try to match with existent rules. Return true on matched otherwise flase.

Router#{hook}(fn:{Function|Array})

Add pre/post hook(s) to all existent url routing rules in the current router instance. Takes one argument which could be function (pre hook) or array of null positioned hooking functions. This method adds pre hook to the left side (begin) of existent pre hooks and adds post hook to right side (end) of existent post hooks.

Router#listen(server:{HttpServer}, /*optional*/ notFound:Function)

Listen to request event of the vanilla node.js HttpServer instance.

App

An app is a class where you define and implement your core business logic. App it self is an event emitter. Properties can be overridden by subclassing existent app.

Define app

You can define an App like this:


  var App = require('genji').App;
  var BlogApp = App(/* instance properties */ properties);

Let's go through a simple example to see how we can define an app.


var App = require('genji').App;

var BlogApp = App({// instance properties object
    /**
     * Name of your app
     */
    name: 'Blog',

    /**
     * App constructor function
     */
    init: function(db) {
      this.db = db;
    },

    /**
     * Create a blog post
     */
    createPost: function(title, content, callback) {
      // Make the post object
      var post = {title: title, content: content, created: new Date()};
      // save to the db
      this.db.save(post).then(function(result){
        // error first callback style
        callback(null, result);
      });
    }
  });

The BlogApp you just defined can be used as a standalone application class. It means you don't need the http stack to run your app code. Once you get the BlogApp class, you initialize and use it just like any other classes. The init function will be called on initialization and it's optional.

Handle result

There are two different ways to handle result of the instance function. One is to add callback as the last argument and handle result inline. Another is to listen to the event emitted from object of app instance.

Inline callback style


var myBlog =  new BlogApp(db);

// create a blog post in db
myBlog.createPost('My awesome post', 'Some contents', function(err, result) {
  // handle error and result here
  // this == myBlog, so you can emit event manually
  this.emit('createPost', err, result);
});

Event style

Default callback

Sometime you don't care about if the post is saved or not. And you may wish to handle the result in other part of your code. So when you call instance function and the last argument is not a callback, genji will generate a default callback for you. You call the callback as usual and an event with the name of the instance method will be triggered.


// the event callback's argument is same as how you call the callback inside the "createPost" function
myBlog.on('createPost', function(err, result) {
  // handle the event
});

//...

// create and forget
myBlog.createPost('I do not care about the result', 'Yes, there is no callback after me.');

There is one exception. When your instance function is synchronized and returns non-undefined value on calling. The event/callback will not be emitted/invoked in async operation.


  getDB: function(cb) {
    cb(); // will call `theCallback`, if no callback gived event will be emitted
    process.nextTick(function(){
      cb(); // `theCallback` will not be called end emit event
    });
    return this.db;
  }

  function theCallback() {
    // this function will never be called in async operation
  }

  var db = myBlog.getDB(theCallback);

Delegation

You can delegate all the events to other event emitter object by setting the delegate property to that emitter.


var otherEmitter = new EventEmitter();

myBlog.delegate = otherEmitter;

// event name will be prefixed by app name on delegation by default
otherEmitter.on('Blog:createPost', function(err, result) {
  // handle the event
});

myBlog.on('createPost', function(err, result) {
  // this will never be called as you allready delegate events to `otherEmitter`
});

myBlog.createPost('title', 'content');

Extending

It's easy to extend an existent app class, use the app class you want to extend just like you use App:


var AwesomeBlogApp = BlogApp({

    name: 'AwesomeBlog',

    createPost: function(title, content, callback) {
      // ...
    },

    getPost: function(id, callback) {
      // ...
    }
  });

Conventions

App introduced a very thin mechanism to organize business logic code. To make app works simply and efficiently with other part of system, here are the conventions you may need to know and understand.

Session

Although we call defined app class, but actually we use individial instance functions in a functional programming style in conjuction with Site and Router. This allow genji to reuse a single app instance for multiple requests instead of constructing for each one of them. You may noticed that, there's no session info passed to createPost in the above example, this is not possible in a real world application. So Genji put the session object which may contains credential or other user specific info as the first argument of app's instance function when working with Site. So your instance function will have some sort of session first callback last style of function signature.


createPost: function(session, title, content, callback) {
  if (session.group === 'author' && session.user) {
    // Make the post object with user info
    var post = {
      user: session.user,
      title: title, content: content, created: new Date()
    };
    this.db.save(post).then(function(result){
      callback(null, result);
    });
  } else {
    // do something
  }
}

To make the app class more reusable, don't put any http stack object as the function argument (e.g. request/response object of node.js etc.). That will make you loose the flexiblity and coupled with http stack. Leave that job to genji.Site to make you life easier.

The this object

You can always use this refer to the instance of app inside of instance function, callback function and event listening function.

Naming and url mapping

Site use name property of app and it's functions' name for mapping url automatically. For example:

Error first callback style

We follow the native node.js api's callback style, which put the error object at the first argument of a callback function. It's true for the event listening function as well.

API

App is exposed by require('genji').App. Inherits from EventEmitter.

Properties

Methods

Site

Site is the organizer for your applications. And using Site is the recommended way to use Genji when you have a large/complex project. It has the following features:

A site object can be created by calling genji.site():

var mySite = genji.site();

Getter/Setter

You can use mySite to set and get settings.


mySite.set('title', 'My Great Website');

// 'My Great Website'
var title = mySite.get('title');

// set a batch of settings
mySite.set({
  host: '127.0.0.1',
  port: 8888
});

// get a batch of settings
var settings = mySite.get(['title', 'host', 'port']);
// {title: 'My Great Website', host: '127.0.0.1', port: 8888}

Use middleware

You don't have to initialize middleware manager by yourself when you use site. You only need to call use method of the site instance.


// use built-in middlewares
mySite.use('conditional-get');
mySite.use('logger', conf);

// use custom middleware

mySite.use(function(core, conf) {
    // do something
  }, conf);

Load app

Using app with site is as simple as middleware. But instead of use you need to call load with app instance.


var blog = new BlogApp();
mySite.load(blog);

// site instance will be setted to app instance's `delegate` property
// blog.delegate === mySite

If you have a lots of apps which have similar initializing options. Then you can set the default app options and let site initialize them for you.

var options = {x: 1, y: 2};
mySite.set('appOptions', options);
mySite.load(BlogApp);
mySite.load(MyOtherApp);

The default options could be overridden and inherited.

var someOptions = {y: 3, z: 2};
// the actual intializing option is {x: 1, y: 3, z: 2}
mySite.load(SomeApp, someOptions);
// load three apps at once
mySite.load([SomeApp1, SomeApp2, SomeApp3], someOptions);

Routing

All methods in the app.publicMethods property will be mapped to url by default follows the convention. For example, the blog.createPost will be mapped to url that matches ^/blog/create/post by default. Of course, if the default convention/mapping does not meet your needs, you can use the map function to map individual url to app method and override the default options.


mySite.map({
  // handle `POST` requests for url '/blog/create/post'
  blogCreatePost: {method: 'post'}
  // handle both `GET` and `POST` requests for url '/blog/read/post/:id'
  blogReadPost: {url: '^/blog/read/post/[0-9]{16}'},
  // add hooks
  blogUpdatePost: {method: 'put', hooks: [preFn1, null, postFn1]}
});

If you have predefined routing definition object, you can load it at the same time when loading app.


var routes = {
  // it's an app default settings if the property key of the route is one of the app's name (in lower case)
  blog: {hooks: [fn1, null, fn2], method: 'get', urlRoot: '^/blog/'},
  blogReadPost: {url: '^/blog/read/post/[0-9]{16}'},
  // a comprehensive route definition
  blogCreatePost: {
    url: '/create/post',
    method: 'post',
    hooks: [fn3, fn4, null, fn5],
    view: 'json' // see [Output result](#output-result)
  }
};

mySite.load(AnotherApp, someOptions, routes);

The routes.blog object holds default settings for all publicMethods of blog instance. It means if some setting is not specified in route, it will use the default one provided by routes.blog. And hooks will be combined. So the final routes of above example normalized by genji would be:

var routes = {
  blogReadPost: {
    url: '^/blog/read/post/[0-9]{16}',
    method: 'get',
    hooks: [fn1, null, fn2],
    view: 'html' // site default value, see [Output result](#output-result)
  },
  // a comprehensive route definition
  blogCreatePost: {
    url: '^/blog/create/post',
    method: 'post',
    hooks: [fn1, fn3, fn4, null, fn5, fn2],
    view: 'json' // see [Output result](#output-result)
  }
};

Output result

We already knew how to map a url to an app's method. Let's see how we can output the result.


  blogReadPost: {url: '^/blog/read/post/[0-9]{16}', view: 'html'}

  blogReadPost: {url: '^/blog/read/post/[0-9]{16}', view: 'json'}

  blogReadPost: {url: '^/blog/read/post/[0-9]{16}', view: function(err, result) {
    if (err) {
      this.error(err);
      return;
    }
    this.sendJSON(result);
  }}

When you use genji's view system, site can render view template automatically for you.


var hogan = require('hogan.js');
mySite.use('view', {engine: hogan, rootViewPath: __dirname});

mySite.map({

  // render template file at "rootViewPath + /blog/views/read_post.html"
  blogReadPost: {url: '^/blog/read/post/[0-9]{16}', view: '/blog/views/read_post.html'},

  // the default behavior is try to find a template file at "rootViewPath + /blog/update_post.html" if you use the view system
  blogUpdatePost: {method: 'put'}

});

Of course, it's possible to do some tweaks and render manually.


  blogUpdatePost: {method: 'put', view: function(err, result) {
    result.someValue = 1;

    // auto template file discovery, render and send
    this.render(result);

    // or indicates template view name manually
    this.render('blog/update_post.html', result);

    var self = this;
    // or render and send manually
    this.render('/path/to/blog/views/update_post.html', result, function (err, html) {
      self.sendHTML(html);
    });
  }}

Environment

By using env you can have different settings, middlewares and apps for different environments. The default environment named default.


// switch to environment "dev"
mySite.env('dev');

// the following title and middleware will be used if your 'process.env.NODE_ENV' === 'dev'
mySite.set('title', '[DEV] My Great Website');
mySite.use('dev-verbose', 'all');
// the "DevApp" is also only avaliable for "dev" environment
mySite.load(DevApp);

// switch back to the 'default' environment
mySite.env(); // or mySite.env('default')

The dev environment inherits default. It means all settings, middlewares and apps setted before you switched to dev are also setted/used/loaded. So when you switched to new environment, set/use/load overrides the differences.

Start your site

Start the server is easy


 mySite.start();

Base

The base module exports two classes

Klass

Klass has follwoing features:

Klass(SuperClass:Function, module:Object, /*optional*/ inherit:Function):Function

Example


  var Klass = require('genji').Klass;

  var Person = Klass(function(name){
    this.name = name;
  }, {
    getName: function(){
      return this.name;
    }
  });

  var Worker = Person({
    init: function(name) {
      this.name += ' Worker';
    },

    work: function(task) {
      return this.getName() + ' start working on ' + task;
    }
  });

  var steve = new Worker('Steve');

  var result = steve.work('writing report');

  // 'Steve Worker start working on writing report' true true
  console.log(result, worker instanceof Worker, worker instanceof Person);

Base

(Coming soon)

CHANGELOG

0.7 (2013)

0.7.0 (2013/03/31)

NOTE: This version is not compatible with version 0.5.x

0.5 (2012)

0.5.10 (2012/12/15)

0.5.9 (2012/12/15)

0.5.8 (2012/11/24)

0.5.7 (2012/11/06)

0.5.6 (2012/10/31)

0.5.5 (2012/10/26)

0.5.4 (2012/09/14)

0.5.3 (2012/08/17)

0.5.2-2 (2012/07/21)

0.5.2-1 (2012/07/18)

0.5.2 (2012/07/05)

0.5.1 (2012/06/16)

0.5.0 (2012/06/05)

0.3.3 (2012/05/19)

0.3.2 (2012/05/08)

0.3.1 (2012/03/12)

0.3.0 (2012/01/05)

0.2.4 (2012/01/02)

0.2.3 (2011/12/15)

0.2.2 (2011/08/29)

0.2.1 (2011/08/11)

0.2.0 (2011/07/10)

0.1.0 (2011/06/29)

0.0.3 (2011/06/13)