gracenode-server

server module for gracenode framework.

Usage no npm install needed!

<script type="module">
  import gracenodeServer from 'https://cdn.skypack.dev/gracenode-server';
</script>

README

Server Module

Server module for gracenode framework that allows you to structurely handle HTTP/HTTPS requests.

This is designed to function within gracenode framework.

How to include it in my project

To add this package as your gracenode module, add the following to your package.json:

"dependencies": {
        "gracenode": "",
        "gracenode-server": ""
}

To use this module in your application, add the following to your gracenode bootstrap code:

var gracenode = require('gracenode');
// this tells gracenode to load the module
gracenode.use('gracenode-server');

To access the module:

// the prefix gracenode- will be removed automatically
gracenode.server

Access

gracenode.server

Configurations

"modules": {
        "gracenode-server": {
                "protocol": "http" or "https",
                "pemKey": "file path to pem key file" // https only
                "pemCert": "file path to pem cert file" // https only
                "secureOptions": "to prevent POODDLE exploit give SSL_OP_NO_SSLv3",  https only
        "port": port number,
                "host": host name or IP address,
                "urlPrefix": optional prefix in the URL to be ignored when routing requests
        "controllerPath": path to controller directory,
        "trailingSlash": true/false // if true is given, trailing slash in request URLs are enforced
                "ignored": ['name of ignored URI'...],
                "error": {
                        "404": {
                                "controller": controller name,
                                "method": public controller method
                        },
                        "500": ...
                },
                "reroute": [
                        {
                                "from": '/',
                                "to": '/another/place'
                        },
                        ...
                ]
        }
}

URL Prefix

If urlPrefix is set in the configurations, the router of gracenode-server module will ignored matched prefix in the URL for routing every request.

Example:

// configurations:
{
    "modules": {
        "gracenode-server": {
            "protocol": "http",
            "port": 8000,
            "urlPrefix": "/dummy/",
            "host": "localhost",
            "controllerPath": "controller/"
        }
    }
}
// incomming request
POST /dummy/mypage/getmyinfo/

// This request will be routed to:
/*
controller: mypage
method: getmyinfo
*/
// /dummy/ will be ignored and discarded

SSL server

gracenode has bash scripts to help set up HTTPS server.

gracenode/scripts/sslcertgen.sh //for production
gracenode/scripts/sslcertgen-dev.sh //for development

Events

requestStart

The event is emitted when a reuqest is received. Passes the request URL.

server.on('requestStart', function (requestUrl) {
        // do something
});

requestEnd

The event is emitted when a request has been handled (not responded). Passes the reuqest URL.

server.on('requestEnd', function (requestUrl) {
        // do something
});

requestFinish

The event is emitted when a request hash been handled and finished sending all response data. Passes the request URL.

server.on('requestFinish', function (requestUrl, executionTime) {
        // do somehting
});

.start

Example of how to set up a server:

// index.js file of an application
var gracenode = require('gracenode');
gracenode.use('server', 'server');
gracenode.setup(function (error) {
        if (error) {
                throw new Error('failed to set up gracenode');
        }
        // we start the server as soon as gracenode is ready
        gracenode.server.start();
});

.addRequestHooks

void addRequestHooks(Object hooks)

Assign a function to be invoked on every request (each hook callback function is assigned to specific controller method).

Should be used for session validatation etc.

Example:

// add a hook to all requests
function hookForAll(request, next) {
        next();
}
gracenode.server.addRequestHooks(hookForAll);
// checkSession will be executed on every request for myController/myPage after the hook for all requests
function checkSession(request, callback) {
        var sessionId = request.cookies().get('sessionId');
        gracenode.session.getSession(sessionId, function (error, session) {
                if (error) {
                        return cb(error);
                }
                if (!session) {
                        // no session
                        return cb(new Error('auth error', 403));
                }
                // session found
                cb();
        });
}
// set up the hook
gracenode.server.addRequestHooks({
        myController: {
                myPage: checkSession
        }
});
// this will apply checkSession function as a request hook to ALL controller and methods
var hooks = checkSession;
// this will apply checkSession function as a request hook to ALL methods of myController
var hooks = {
        myController: checkSession
};
// set up request hooks
gracenode.server.addRequestHooks(hooks);
// this will apply checkSession function as a request hook to myPage of myController only
var hooks = {
        myController: {
                myPage: checkSession
        }
};
// set up request hooks
gracenode.server.addRequestHooks(hooks);

How to assign multiple hooks

.addRequestHooks can let you assign more then one hook function to specific controller/method or even to all reuqests:

gracenode.addRequestHooks({
        myController: [
                hook1,
                hook2,
                hook3
        ]
});

The assigned hooks will be executed in the order of the array given.


.addResponseHooks

void addResponseHooks(Object hooks);

Assign a function to be invoked on every response (each hook callback function is assigned to specific controller method).

This is useful when you need to execute certian operation on every request response

The basic behavior of response hooks are very similar to reqest hooks.

Example:

function hookForAll(request, cb) {
        cb();
}

function writeTrackingData(requestObject, cb) {
        // write tracking data based on request data
        if (error) {
                return cb(new Error('failed'));
        }
        // success
        cb();
}

gracenode.server.addResponseHooks(hookForAll);

gracenode.server.addResponseHooks({
        myController: writeTrackingData
});

Above assignment of hooks will execute hookForAll on EVERY response and writeTrackingData after on /myController

How to assign multiple hooks

.addResponseHooks can let you assign more then one hook function to specific controller/method or even to all responses:

gracenode.addResponseHooks({
        myController: [
                hook1,
                hook2,
                hook3
        ]
});

The assigned hooks will be executed in the order of the array given.

.getControllerMap

Returns mapped controllers and their methods.

.getEndPointList

Returns an array of all URL endpoints.


Controller

Controller handles all requests to server module.

Example:

// controller/example/foo.js /example/foo/
var gracenode = require('gracenode');
// the first argument is **ALWAYS** requestObject
// this will handle requests with "GET" method ONLY
module.exports.GET = function (requestObject, serverResponse) {
        // serverResponse is created by server module per request
        serverResponse.json({ foo: 'foo' });
};
// /example/foo/ will display "foo" on your browser

Translating request URL to controller/method

gracenode's server module translates request URL to route all requests to correct controllers and methods and pass the rest of request parameters to the controller method as reuqest.parameters [array].

// Suppose we have a request like this: GET mydomain.com/myController/myMethod/a/b/c/d
// controller translates this as:
// myController/myMethod.js
module.exports.GET = function (request, response) {
        var params = request.parameters;
        /*
        [
                "a",
                "b",
                "c",
                "d"
        ]
        */
};

Pre-defined URL parameteres

Each controller method can optionally have pre-defined parameter names.

To added pre-defined paramter names, add the following in your controller method:

Example:

// request URL yourapp.com/example/test/myParam1/myParam2/
module.exports.params = [
    'foo',
    'boo'
];
module.exports.GET = function (request, response) {
    var foo = request.getParam('foo');
    // foo is 'myParam1'
    var boo = request.getParam('boo');
    // boo is 'myParam2'
};

Subdirectories

gracenode-server module supports subdirectories in your controller and methods.

For example, a request URI such as /yourController/yourMethod/sub/foo would be translated as:

{
    controller: 'yourController',
    method: 'yourMethod',
    params: [
        'sub',
        'foo'
    ]
}

However, by creating a request handling method in /yourController/yourMethod/sub/foo.js, server will be executing /yourController/yourMethod/sub/foo.js instead of /yourController/yourMethod.js.

**NOTE:**Both request hooks and response hooks can also be applied to specific subdirectory methods.

Request Method Restrictions

Each controller method accepts and executes requests with declared request method ONLY.

Example:

// POST /exmaple/boo -> exmaple/boo.js
module.exports.POST = function (req, res) {
        // do something
};

Above example is the controller method for POST requests (POST /example/boo).

If any other request method than POST is sent, the server will response with an error (status 400).

Request URL

Request URL can be accessed as shown below.

module.exports.GET = function (requestObject, response) {
        var url = requestObject.url;
};

How to read GET, POST, PUT, and DELETE

Request data can be accessed according to the request methods as shown below.

// read GET data
module.exports.GET = function (requestObject, response) {
        // server module supports GET, POST, PUT, or DELETE
        var foo = requestObject.data('foo');
        var allData = requestObject.dataAll();
        response.json(null);
};
// read POST data
module.exports.POST = function (requestObject, response) {
        // server module supports GET, POST, PUT, or DELETE
        var foo = requestObject.data('foo');
        var allData = requestObject.dataAll();
        response.json(null);
};
// read PUT data
module.exports.PUT = function (requestObject, response) {
        // server module supports GET, POST, PUT, or DELETE
        var foo = requestObject.data('foo');
        var allData = requestObject.dataAll();
        response.json(null);
};
// read DELETE data
module.exports.DELETE = function (requestObject, response) {
        // server module supports GET, POST, PUT, or DELETE
        var foo = requestObject.data('foo');
        var allData = requestObject.dataAll();
        response.json(null);
};

Example:

// sent data from client: { "value": "12345" } with request header Content-Type: application/json
module.exports.GET = function (request, response) {
        // this will return "12345"
        var literalStr = request.data('value', true);
        // this will return 12345
        var int = request.data('value');
};

Auto Request Body Data Validation

gracenode-server can optionally validate incoming request data of every request.

To set up auto request body data validation, you must add a property called expected in your controller method file.

Exmaple (request URL: /hello/world/):

// each key of expected holds a validation function that returns an error for invalid value
exports.expected = {
    message: function validation(value) {
        if (!value) {
            return new Error('message must be given');
        }
        if (typeof value !== 'string') {
            return new Error('message must be a string');
        }
    }
};
exports.GET = function (request, response) {
    // message has already been validate and it is safe for us to use its value!
    var message = request.data('message');
};

The above example defines an expected request body data called message.

This API now requires every request to this end point to have message and the value of the message MUST validate with the associated validation function.

If validation fails, the request will respond with 400 status code.

Dealing With Uploaded Files

gracenode-server has 2 functions to deal with uploaded files.

The functions are methods of a request object.

request.moveUploadedFile(path [string], newPath [string], cb [function])

Moves an uploaded file from temporary path to a new location.

Typically, the uploaded files are located in /tmp/ directory

Example:

exports.PUT = function (request, response) {
    var files = request.data('files');
    request.moveUploadedFile(files[0].path, newPath, function (error) {
        if (error) {
            return response.error(error, 500);
        }
        response.json({ message: 'uploaded' });
    });
};

request.getUploadedFileData(path [string], cb [function])

Reads the data from an uploaded file and deletes the file.

Example:

exports.PUT = function (request, response) {
    var files = request.data('files');
    request.getUploadedFileData(files[0].path, function (error, data) {
        if (error) {
            return response.error(error, 500);
        }
        // do something with the read data...
        response.json({ message: 'uploaded and read' });
    });
    
};

Accessing Controller Name and Controller Method Name:

// controller file
// URI: /test/one/
module.exports.GET = function (requestObject, response) {
        console.log(requestObject.controller);
        // "test"
        console.log(requestObj.method);
        // "one"
};

NOTE: Subdirectory methods would return the path to the subdirectory method as the method name.

Example:

// URI: /test/sub/foo/
module.exports.GET = function (requestObject, response) {
    var methodName = requestObject.method;
    // sub/foo
};

Request Headers

Request headers are accessed as:

// controller file
module.exports.GET = function (requestObject, response) {
        // server module automatically gives every contrller the following function:
        // requestObject.headers an instance of Headers class
        var os = requestObject.headers.getOs();
};

Headers class

Headers object holds all request headers and related methods.

Access:

module.exports.GET = function (requestObject, response) {
       var requestHeaders = requestObject.headers;
};

headers.get

String get(String headerName)

headers.getOs

String getOs()

headers.getClient

String getClient()

headers.getDefaultLang

String getDefaultLang

Cookies

This object holds and contains all cookie related data and methods.

// controller
module.exports.GET = function (requestObject, response) {
        var cookies = requestObject.cookies();
        // get
        var foo = cookies.get('foo');
        // set
        cookies.set('boo', 'boo');
};

Response class

Response class manages all server responses to the client requests.

response.getRequest

Returns an instance of a request object.

response.header

Response headers can be set/removed as shown below.

Example for adding a response header

// controller
module.exports.GET = function (requestObject, response) {
        // name, value
        response.header('foo', 'foo');
};

Example for removing a response header

module.exports.GET = function (requestObject, response) {
    // remove header
    response.header('Pragma', null);
};

response.json

Resonds to the client as JSON.

Status code is optional and default is 200.

Void response.json(Mixed content, Integer status)

response.html

Resonds to the client as HTML.

Status code is optional and default is 200.

Void response.html(String content, Integer status)

response.error

Responds to the client as an error. content can be JSON, String, Number.

Status code is optional and default is 404.

Void response.error(Mixed content, Integer status)

response.stream

Streams a file to the client.

If request header contains range header, it will stream with status 206.

Void response.stream(String filePath, String fileType)

NOTE: File types are: mp4, ogg etc.

response.download

Let the client download data as a file.

Void response.download(String data, String fileName, int status);

response.data

Response to the client with raw data. Used to let the client download data as a file.

Typically, you should be using response.download() instead for file downloads.

Void response.data(String data)

Example:

// API to download a CSV file format
module.exports.GET = function (requestObj, response) {
        var filename = 'test.csv';
        var csvData = 'columnA,columnB\nAAA,BBB\nCCC,DDD\nEEE,FFF';
        // set response headers
        response.header('Content-Disposition', 'attachment; filename=' + filename);
        response.header('Content-Type', 'csv');
        response.data(csvData);
};

response.redirect

Redirects the request to another URL with HTTP status 307.

// controller
// request URI /foo/index/
module.exports.GET = function (requestObject, response) {
        response.redirect('/anotherPage/');
};

To redirect with other status code than 307, for example 301:

module.eports.GET = function (requestObject, response) {
    response.redirect('/redirect/to/this/page', 301);
};

response.file (Not recommanded)

Resonds to the client as a static file. Status code is optional and default is 200.

Void response.file(Binary content, Integer status)

Configuring Error Handlers

Server module allows the developer to assigned specific error handling constroller end method based on HTTP response status

Example:

// configurations:
{
        "server": {
                "404": {
                        "controller": "error",
                        "method": "notFound"
                }
        }
}

The above example assigns the controller "error" and method "notFound" (controller/error/notFound.js) to status code 404.

When the server responds with status 404, the server will then execute error/notFound.js.

Building a Web Server

gracenode has a built-in module called "server". This module allows you to create and run either HTTP or HTTPS server.

For more details about server module please read here.

How to tell gracenode to use server module

// this is your application index.js
var gn = require('gracenode');

// tell gracenode where to look for configuration file(s)
// gracenode always looks from the root path of your application
gn.setConfigPath('configurations/');

// tell gracenode which configuration file(s) to load
gn.setConfigFiles(['config.json']);

// tell gracenode to load server module
gn.use('server');

gn.setup(function (error) {
    if (error) {
        return console.error('Fatal error on setting up gracenode');
    }

    // gracenode is now ready
    // start the server
    gn.server.start();

});

How to configure server module

Please refer to server module README for more details.

// this the minimum requirements for server module to run
{
    "modules": {
        "server": {
            "protocol": "http",
            "host": "localhost",
            "port": 8000,
            "controllerPath": "controller/"
        }
    }
}

How to create your "Hello World" page

Assuming that you are using configurations like the above, we can create our "Hello World" controller in:

yourapp/controller/helloworld/

Let's assume that our URL for "Hellow World" page would be "http://yourapp.com/hellowworld/sayhello".

Server module translates the above URL to be like this:

"helloworld" after the domain is interpreted to be the controller directory as yourapp/controller/helloworld/.

"sayhello" is your actual controller and it would be assumed to be yourapp/controller/helloworld/sayhello.js.

Add the controller logic to sayhello.js

We will assume that this URL will be a GET request.

// this is what's inside sayhello.js
// server controller will always recieve a request object and response object on each request
// notice that we specifically say "GET". this is telling server module to handle GET request only.
module.exports.GET = function (request, response) {
    // since there isn't much to do, we will send the response back to the client right away
    response.html('<h1>Hello World</h2>');
};

More Advanced Features

There are more complex things you can do with server module. For example rerouting is one of them.

How to reroute a URL to a specific controller and its method

Assume we want to have a URL like this "http://yourapp.com/helloworld".

But we want to execute yourapp/controller/helloworld/sayhello.js for this URL.

This kind of rerouting can be achieved by setting "reroute" in the configurations.

{
    "modules": {
        "server": {
            "protocol": "http",
            "host": "localhost",
            "port": 8000,
            "controllerPath": "controller/",
            "reroute": [
                { "from": "/", "to": "helloworld/sayhello" }
            ]
        }
    }
}

Notice we have a new configuration object called "reroute" in the above configurations.

This configuration allows server module to execute helloworld/sayhello.js when the server receives a reuqest to "http://yourapp.com/helloworld".

Assign uniformted error controllers on specific error status

Server module also allows you to pre-define controllers to be executed on specific errors.

For example if your want to display a certain page for "404 Not Found" error, we can assign a specific controller and method for it.

{
    "modules": {
        "server": {
            "protocol": "http",
            "host": "localhost",
            "port": 8000,
            "controllerPath": "controller/",
            "reroute": [
                { "from": "/", "to": "helloworld/sayhello" }
            ],
            "error": {
                "404": {
                    "controller": "error",
                    "method": "notFound"
                }
            }
        }
    }
}

Notice we have a configuration object called "error" in the above configurations.

This tells server module to execute yourapp/controller/error/notFound.js on HTTP status 404.

Request Hooks

Server module can let you assign certain function(s) to be executed on requests.

This is usefuly for session validation on requests etc.

Example:

gracenode.setup(function () {

        // assign session validation function to all requests under "example" controller
        gracenode.server.addRequestHooks({
                example: function (request, callback) {
                        if (isSessionValid()) {
                                // session is valid. continue to execute the request
                                return cb();
                        }
                        // session is not valid. respond with error
                        cb({ code: 'invalidSession' }, 403);
                }
        });
});