Node Atlas NodeAtlas

The Progressive Server-side JavaScript Framework

  • Simple set-up

    You know HTML & CSS?
    But not JavaScript?

    Quickly create multilingual websites effortlessly with simple routes, views or variations.

  • Scalable website

    JavaScript client-side Expert?
    Ready to embrace Node.js?

    Gradually improve your base as you need by using controllers, models or modules.

  • Agnostic client side

    Already your Front-end habits?
    You use Data Binding?

    From Vanilla to jQuery and going through Vue, Angular or React: use your favorite tools!

Advanced Part

NodeAtlas offers also a large set of features for development or packaging with the configuration system. We will see that.

Page Not Found

Listen all URLs, and also file provide by assetsRelativePath

To display a custom page when a resource is not found you must:

  1. Prepare a 404 page.
  2. Fill the parameter with pageNotFound with the following value : key of the prepared 404 page.

See the example below:

{
    "pageNotFound": "/not-found-page/",
    "routes": {
        "/list-of-members/": {
            "view": "members.htm"
        },
        "/": {
            "view": "index.htm"
        },
        "/not-found-page/": {
            "view": "error.htm",
            "statusCode": 404
        }
    }
}

you can access to:

Localized Error Page

For this, just create a new route with * at the end in language you want.

See below :

{
    "pageNotFound": "/not-found-page/",
    "languageCode": "en-gb",
    "routes": {
        "/list-of-members/": {
            "view": "members.htm",
            "variation": "members.json"
        },
        "/": {
            "view": "index.htm",
            "variation": "index.json"
        },
        "/not-found-page/": {
            "view": "error.htm",
            "variation": "error.json",
            "statusCode": 404
        },
        "/francais/liste-des-membres/": {
            "view": "members.htm",
            "languageCode": "fr-fr",
            "variation": "members.json"
        },
        "/francais/": {
            "view": "index.htm",
            "languageCode": "fr-fr",
            "variation": "index.json"
        },
        "/francais/*": {
            "view": "error.htm",
            "languageCode": "fr-fr",
            "variation": "error.json",
            "statusCode": 404
        }
    }
}

Dynamic Routing

Although you can configure static URLs, you can also set of dynamic URLs!

Parameters

It is possible to get some parameters from URL to display a different content depending of slugs.

With the following configuration:

{
    "routes": {
        "/list-of-members/:member/:action/": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/list-of-members/:member/:action": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/list-of-members/:member/": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/list-of-members/:member": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/list-of-members/": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/list-of-members": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/": {
            "view": "index.htm"
        }
    }
}

you can access to:

and retrieve the :member, :action, query and test value in changeVariations (common and specific).

exports.changeVariations = function (next, locals, request, response) {

    console.log("param request:", request.params.member);
    // $ undefined, 'toto', 'bob-eponge99', 'node-atlas' or 'etc'.
    console.log("param locals:", locals.params.member);
    // $ undefined, 'toto', 'bob-eponge99', 'node-atlas' or 'etc'.

    console.log("param request", request.params.action);
    // $ undefined, 'show' or 'lolol'.
    console.log("param locals", locals.params.action);
    // $ undefined, 'show' or 'lolol'.

    console.log("query request", request.query.example);
    // $ undefined or 'test'
    console.log("query locals", locals.query.example);
    // $ undefined or 'test'

    console.log("body request", request.body.test);
    // $ undefined or 'This is a test'.
    console.log("body locals", locals.body.test);
    // $ undefined or 'This is a test'.

    next();
};

Advanced Parameters

We can see which we use the same config for three routes in the previous example. You could also use regular expressions to define that is variable into your URL or define what are the valid parameters in your URL. This system is less complex than real RegExp because a lot of char does not exist in URL so, for example, this char / do not need to be escaped.

With the following configuration:

{
    "routes": {
        "/list-of-members/?(:member([-a-zA-Z0-9]+)/?(:action(show|edit)/?)?)?": {
            "view": "members.htm"
        },
        "/": {
            "view": "index.htm"
        }
    }
}

you can access to:

and retrieve the :member, :action, query and test value in a view.

<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <title>URL Rewriting Test</title>
  </head>
  <body>
    Member: <strong><?- params.member ?></strong><br>
    Action: <strong><?- params.action ?></strong><br>
    Example: <strong><?- query.example ?></strong><br>
    Test: <strong><?- body.test ?></strong>
  </body>
</html>

you cannot access to:

Regular Expressions

You can also enable regular expressions to a specific path with regExp. If it is true, the previous profile no longer works and you pass in Regular Expression mode. If regExp is a string, it acts as a flag (g, i, m or y).

See the following configuration:

{
    "routes": {
        "/list-of-members/([-a-z0-9]+)/?": {
            "view": "members.htm",
            "regExp": "i"
        },
        "/list-of-members/?": {
            "view": "members.htm",
            "regExp": true
        },
        "/": {
            "view": "index.htm"
        }
    }
}

you can access:

and retrieve the ([-a-z0-9] +) value in the changeVariations (common and specific).

exports.changeVariations = function (next, locals) {

    if (locals.params && locals.params[0]) { locals.params.member = locals.params[0]; }
    // locals.params[1] for second match, etc...

    console.log(locals.params.member);
    // $ 'toto', 'bob-eponge99', 'node-atlas' or 'etc'.

    next();
}

The rules for creating dynamic URL with regExp are those of RegExpJavaScript.

Programmatic Routing

setRoutes allows us to dynamically inject routes. However, the route injection add a route at the end of NA.webconfig.routes because NA.webconfig.routes is an object. There are no possibility to order routes, but this is a problem because routes path are resolved in order of injection.

We will resolved that with new way to set routes from routes: { <key>: { ... } } to routes: [{ "key": <key>, ... }].

This is all files for example:

├─ controllers/
│  └─ common.js
├─ views/
│  ├─ index.htm
│  ├─ content.htm
│  └─ error.htm
└─ webconfig.json

With the webconfig.json originaly like this routes: <Object>:

{
    "controller": "common.js",
    "routes": {
        "/doc/index.html": {
            "view": "index.htm"
        },
        "/doc/*": {
            "view": "error.htm",
            "statusCode": 404
        }
    }
}

and transformed like this routes: <Array>:

{
    "controller": "common.js",
    "routes": [{
        "url": "/doc/index.html",
        "view": "index.htm"
    }, {
        "url": "/doc/*",
        "view": "error.htm",
        "statusCode": 404
    }]
}

With the common.js file, it's now possible to inject routes at the position we want. We will see an example at the first position.

// This code is executing while route are added.
// This code will be executed when NodeAtlas starting.
exports.setRoutes = function (next) {

    // We use instance of NodeAtlas.
    var NA = this,

        // And we keep routes from NodeAtlas webconfig...
        route = NA.webconfig.routes;

    // ...to add `/content.html` route in first place.
    route.unshift({
        "url": "/doc/content.html",
        "view": "content.htm"
    });

    // We update modification here.
    next();
};

In this way, address http://localhost/doc/content.html will return the content.htm view and not the error.htm view with 404.

Redirects

To go to a different address (redirect 301 or 302) when you get to an URL you must use the redirect parameter.

Note: if you don't set statusCode, no redirect will be executed. The statusCode is mandatory for redirection.

Static

See the example below:

{
    "routes": {
        "/list-of-members/": {
            "view": "members.htm"
        },
        "/list-of-members": {
            "redirect": "/list-of-members/",
            "statusCode": 301
        },
        "/go-to-node-atlas/": {
            "redirect": "https://node-atlas.js.org/",
            "statusCode": 302
        },
        "/": {
            "view": "index.htm"
        }
    }
}

You will be redirected:

  • to http://localhost/list-of-members/ when you access http://localhost/list-of-members with a header permanent redirect.
  • to https://node-atlas.js.org/ when you access http://localhost/go-to-node-atlas/ with a header temporary redirect.

Dynamic

See the example below:

{
    "routes": {
        "/list-of-members/:member/": {
            "view": "members.htm"
        },
        "/list-of-members/:member": {
            "redirect": "/list-of-members/:member/",
            "statusCode": 301
        },
        "/": {
            "view": "index.htm"
        }
    }
}

You will be redirected to http://localhost/list-of-members/haeresis/ when you access to http://localhost/list-of-members/haeresis with a header permanent redirect.

With regular expressions

See the example below:

{
    "routes": {
        "/membres/([-a-z0-9]+)/": {
            "view": "members.htm",
            "regExp": true
        },
        "/list-of-members/([-a-z0-9]+)/": {
            "redirect": "/membres/$0/",
            "statusCode": 301,
            "regExp": true
        },
        "/list-of-members/": {
            "view": "members.htm"
        },
        "/": {
            "view": "index.htm"
        }
    }
}

You will be redirected to http://localhost/list-of-members/haeresis/ when you access to http://localhost/list-of-members/haeresis with a header permanent redirect.

For the second match use $1, the third $2, etc.

HTTP Headers

By défault, sent headers by NodeAtlas are followings: Content-Type:text/html; charset=utf-8 with a 200 statusCode.

It's possible to modify this values for a specific route (for local API for example).

{
    "mimeType": "application/json",
    "charset": "utf-16",
    "routes": {
        "/": {
            "view": "index.htm",
            "mimeType": "text/html"
        },
        "/api/articles": {
            "view": "display-json.htm",
            "controller": "blog/list-of-articles.js",
            "charset": "utf-8",
            "statusCode": 203
        }
    }
}

It's also possible to modify all headers values, this erase all shortcuts before (except the statusCode). Set a value to false remove this header previously setted.

{
    "headers": {
        "Content-Type": "application/json; charset=utf-8",
        "Access-Control-Allow-Origin": "*"
    },
    "routes": {
        "/api/articles": {
            "view": "display-json.htm",
            "controller": "blog/list-of-articles.js",
            "statusCode": 203,
            "headers": {
                "Access-Control-Allow-Origin": false
            }
        }
    }
}

Dynamic Configuration

In replacement of static .json config files, you can use dynamic .js config files. In this case, your .js file can provide in module.exports a valide JSON file.

And it is possible to replace this six following files:

webconfig.json

{
    "languageCode": "fr-fr",
    "statics": "statics.fr-fr.json"
    "routes": {
        "/": "index.htm"
    }
}

webconfig.prod.json

{
    "cache": true,
    "languageCode": "fr-fr",
    "statics": "statics.fr-fr.json",
    "routes": {
        "/": "index.htm"
    }
}

webconfig.en-us.json

{
    "languageCode": "fr-fr",
    "statics": "statics.fr-fr.json",
    "routes": {
        "/": "index.htm"
    }
}

webconfig.en-us.prod.json

{
    "cache": true,
    "languageCode": "en-us",
    "statics": "statics.en-us.json",
    "routes": {
        "/": "index.htm"
    }
}

statics.fr-fr.json

{
    "/variations/": "variations/fr-fr/",
}

statics.en-us.json

{
    "/variations/": "variations/en-us/",
}

by only this two following files:

webconfig.js

module.export = (function () {
    var webconfig = {
        "cache": false,
        "languageCode": "fr-fr",
        "statics": "statics.json"
        "routes": {
            "/": "index.htm"
        }
    };

    if (process.env.NODE_ENV === 'production') {
        webconfig["cache"] = true;
    }

    if (process.env.LANG) {
        webconfig["languageCode"] = process.env.LANG;
    }

    return webconfig;
}());

statics.js

module.export = (function () {
    var NA = this.NA,
        languageCode = NA.webconfig.languageCode

    return {
        "/variations/": "variations/" + languageCode + "/",
    };
}());

with the following supposed set of environment variables on the four following environments:

Local FR

NODE_ENV=DEVELOPMENT
LANG=fr-fr

Local EN

NODE_ENV=DEVELOPMENT
LANG=en-us

Prod FR

NODE_ENV=PRODUCTION
LANG=fr-fr

Prod EN

NODE_ENV=PRODUCTION
LANG=en-us

HTTPs

It is very simple to run an instance of NodeAtlas with HTTPs protocol. You just have to create such a security folder in which to place your server.key and server.crt file to supply the protocol.

Just use the following configuration:

{
    "httpSecure": true,
    "httpSecureKeyRelativePath": "security/server.key",
    "httpSecureCertificateRelativePath": "security/server.crt",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

Alternatively , if your two .key and .crt files have the same name, use this configuration:

{
    "httpSecure": "security/server",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

This is also possible to just set the httpSecure value to true to get a "https" like urlBasePath or urlBase in your paths variables. But the server will not run in HTTPs and you will validate certificate by your own way (with a server proxy for example).

{
    "httpSecure": true,
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

Note: in production, if you use a proxy for redirect request/response, don't forget use urlPort: 443 instead of urlPort: 80 for HTTPs.

GET / POST

You can also manager how the server will respond to requests GET/POST to a given page. For example, we will allow access to pages only GET for the whole site and allow a POST to one page only (and prohibited him GET).

{
    "get": true,
    "post": false,
    "routes": {
        "/": {
            "view": "index.htm"
        },
        "/list-of-members/": {
            "view": "members.htm"
        },
        "/write-comment/": {
            "view": "write-com.htm"
        },
        "/save-comment/": {
            "view": "save-com.htm",
            "get": false,
            "post": true
        }
    }
}

Note: if nothing is set, get and post are set to true in global webconfig and by route.

PUT / DELETE

They work in the same way as get and post. This two HTTP actions PUT and DELETE are by default not activated. To active it use put and delete.

{
    "get": false,
    "post": false,
    "put": true,
    "routes": {
        "/read-all-entry/": {
            "view": "display-json.htm",
            "variation": "all-entry.json",
            "get": true,
            "put": false
        },
        "/read-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "get": true,
            "put": false
        },
        "/create-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "post": true,
            "put": false
        },
        "/update-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json"
        },
        "/delete-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "delete": true,
            "put": false
        }
    }
}

With the configuration below, only one HTTP action is possible by route, this is a great way to create APIs REST easily with NodeAtlas.

CORS and OPTIONS

By default preflighted requests are not enable. You will need its, for example, to do CORS requests. Prefilghted requests are send with OPTIONS HTTP method.

To activate OPTIONS for a route, use the options property on a route of the webconfig. To activate OPTIONS on all routes, use in this case the property options in the webconfig in global.

{
    "options": true,
    "routes": {
        "/read-all-entry/": {
            "view": "display-json.htm",
            "variation": "all-entry.json",
            "options": false
        },
        "/create-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "post": true
        },
        "/delete-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "delete": true
        }
    }
}

Cross-Domain Request

If you want authorize a ressource on the NodeAtlas server requested by domain www.domain-a.com for a single page, you could do like this:

{
    "routes": {
        "/api/random-quote": {
            "controller": "get-quote.js",
            "headers": {
                "Access-Control-Allow-Origin": "http://www.domain-a.com"
            }
        }
    }
}

With that, you will be able to accept for example a request from Origin, http://www.domain-a.com which is a value in Access-Control-Allow-Origin:

GET /api/random-quote HTTP/1.1
Host: www.domain-a.com
...
Origin: http://www.domain-a.com
...

Cross-Domain Request with Token

If you want authorize resources from NodeAtlas server to the request from anywhere for the /api/random-quote page and the page /api/protected/random-quote that claims an authentification token, you could do that like this:

{
    "mimeType": "application/json",
    "headers": {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Authorization"
    },
    "routes": {
        "/api/random-quote": {
            "controller": "get-quote.js"
        },
        "/api/protected/random-quote": {
            "controller": "get-quote.js",
            "middlewares": "is-authenticated.js",
            "options": true
        }
    }
}

NodeAtlas will parse a token from the external domain if this token is sent by Authorization headers in the request. For allows NodeAtlas to accept this request, define it into Access-Control-Allow-Headers with the accepted value Authorization. Send a token need a preflight request so it's required to set options to true to authorize HTTP Request with OPTIONS method.

Now, you will be able to accept for example the following request which sends an authentification token to our server for the /api/protected/random-quote resource:

GET /api/protected/random-quote HTTP/1.1
Host: localhost:1337
...
Origin: http://localhost
Authorization: Bearer CODE_DU_JETON
...

Other Cross-Domain requests

All headers for CORS features are accepted by the headers adding mechanism of NodeAtlas.

Settings of Sessions

Key and Secret

NodeAtlas itself manages sessions stored on the server as initial settings:

  • Key : nodeatlas.sid
  • Secret : 1234567890bépo

that allows customers to stay connected through the pages to a single set of personal server-side variable.

It is possible to change the default settings (and even compulsory for productions sites) with the parameters of webconfig.json following:

{
    "sessionKey": "personal key",
    "sessionSecret": "personal secret"
}

NodeAtlas also employs a memory storage object (MemoryStore) to stock the information in the RAM of the server.

Other Parameters

It is possible to change all the parameters of the sessions (except MemoryStore) using the configuration of next webconfig.json:

{
    "session": {
        "key": "personal key",
        "secret": "personal secret",
        "cookie": {
            "path": "/",
            "httpOnly": true,
            "secure": false,
            "maxAge": null
        },
        ...,
        ...,
        ...
    }
}

The entirety of the possible configuration is located on the module documentation express-session.

Storage Sessions

By default, this is NodeAtlas server that stores sessions in the RAM of the server application. This does not allow users to share sessions across multiple applications NodeAtlas (or other) and erases all current sessions for an application if you restart it.

To address this concern, it should support the recording sessions via a base No SQL such as Redis or MongoBD.

You just have to use the setSessions function in common controller file.

Session managed with Redis

Implement the following code in the common controller file to store your sessions in a local Redis.

exports.setModules = function () {
    var NA = this;

    NA.modules.RedisStore = require("connect-redis");
};

exports.setSessions = function (next) {
    var NA = this,
        session = NA.modules.session,
        RedisStore = NA.modules.RedisStore(session);

    NA.sessionStore = new RedisStore();

    next();
};

More information to connect-redis page.

Session managed with MongoDB

Implement the following code in controllers/common.js to store sessions in the database sessions of a local MongoDB.

exports.setModules = function () {
    var NA = this;

    NA.modules.MongoStore = require("connect-mongo");
};

exports.setSessions = function (next) {
    var NA = this,
        session = NA.modules.session,
        MongoStore = NA.modules.MongoStore(session);

    NA.sessionStore = new MongoStore({
        db: "sessions"
    });

    next();
};

More information to connect-mongo page.

Listening URL

It is possible to generate a different URL listening other port with urlHostname and urlPort. For example, the local loop listens on port 80 for a script makes the Reverse Proxy from the port 7777 on the 80 with the "http-proxy" module as below:

{
    "httpPort": 7777,
    "httpHostname": "127.0.0.1",
    "urlPort": 80,
    "urlHostname": "localhost",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

Dynamic URLs

Relative paths in absolute

It is possible that the paths created from your URL to be interpreted as subfolders that have actually no real existence. This has the effect the address media/images/example.jpg initially accessible from template displayed to address http://localhost impossible to reach when the template is displayed to address http://localhost/sub-directory/ (because the path should be ../media/images/example.jpg).

To no longer have to worry about access to resources regardless of the URL that is requested, simply turn on all the URLs such as:

<link rel="stylesheet" type="text/css" href="stylesheets/common.css" />
<!-- ... -->
<img src="media/images/example.jpg" />
<!-- ... -->
<script type="text/javascript" src="javascripts/common.js"></script>

in absolute URLs with variable urlBasePath as below:

<link rel="stylesheet" type="text/css" href="<?= urlBasePath ?>stylesheets/common.css" />
<!-- ... -->
<img src="<?= urlBasePath ?>media/images/example.jpg" />
<!-- ... -->
<script type="text/javascript" src="<?= urlBasePath ?>javascripts/common.js"></script>

Note that in the case of the following configuration:

{
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

urlBasePath return http://localhost/ while in this configuration:

{
    "httpPort": 7777,
    "urlRelativeSubPath": "sub/folder",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

urlBasePath return http://localhost:7777/sub/folder/.

The paths of templates

Using the following webconfig:

{
    "routes": {
        "/index.html": {
            "view": "index.htm"
        },
        "/contact.html": {
            "view": "contact.htm"
        }
    }
}

and the corresponding template

<!-- ... -->
<a href="http://localhost/index.html">Link to home</a>
<a href="http://localhost/contact.html">Link to contact</a>
<!-- ... -->

I'd have to change my link in the template if I change the listening port or if I change the path of the URL. The following configuration changes:

{
    "httpPort": 7777,
    "routes": {
        "/home.html": {
            "view": "index.htm"
        },
        "/contact-us.html": {
            "view": "contact.htm"
        }
    }
}

force me to modify the previous template like that:

<!-- ... -->
<a href="http://localhost:7777/home.html">Link to home</a>
<a href="http://localhost:7777/contact-us.html">Link to contact</a>
<!-- ... -->

You can solve this problem by giving a key to a specific path and deporting are way in the url property.

With the followinh webconfig:

{
    "routes": {
        "index": {
            "url": "/index.html",
            "view": "index.htm"
        },
        "contact": {
            "url": "/contact.html",
            "view": "contact.htm"
        }
    }
}

I can now write the link in the dynamic template:

  1. as follows

    <!-- ... -->
    <a href="<?= urlBasePath ?><?= webconfig.routes.home.url.slice(1) ?>">Link to home</a>
    <a href="<?= urlBasePath ?><?= webconfig.routes.contact.url.slice(1) ?>">Link to contact</a>
    <!-- ... -->
    

    Note: .slice(1) makes it easy to remove the dual / for standard URL.

  2. or as follows

    <!-- ... -->
    <a href="<?= urlBasePath ?>.<?= webconfig.routes.home.url ?>">Link to home</a>
    <a href="<?= urlBasePath ?>.<?= webconfig.routes.contact.url ?>">Link to contact</a>
    <!-- ... -->
    

    Note: This would, for example http://localhost/./home.html, which is a standard URL.

  3. ou comme suit

    <!-- ... -->
    <a href="<?= urlBasePathSlice + webconfig.routes.home.url ?>">Link to home</a>
    <a href="<?= urlBasePathSlice + webconfig.routes.contact.url ?>">Link to contact</a>
    <!-- ... -->
    

    Note : urlBasePathSlice return http://localhost in place of http://localhost/ or http://localhost:7777/sub/folder in place of http://localhost:7777/sub/folder/.

Utilisation de la clé pour mapper les pages

It's maybe useful to know the key used for the current page displayed for find the equivalent page in an other language.

With the following webconfig:

{
    "languageCode": "en-us",
    "routes": {
        "index_en-us": {
            "url": "/",
            "view": "/index.htm"
        },
        "index_fr-fr": {
            "url": "/francais/",
            "view": "index.htm",
            "languageCode": "fr-fr"
        },
        "cv_en-us": {
            "url": "/resume/",
            "view": "cv.htm"
        },
        "cv_fr-fr": {
            "url": "/francais/cv/",
            "view": "index.htm",
            "languageCode": "fr-fr"
        }
    }
}

and the common variation following:

{
    "language": [{
        "name": "English",
        "code": "en-us"
    }, {
        "name": "French",
        "code": "fr-fr"
    }]
}

in fr :

{
    "language": [{
        "name": "Anglais",
        "code": "en-us"
    }, {
        "name": "Français",
        "code": "fr-fr"
    }]
}

we could create link between each page as following:

<ul>
    <? for (var i = 0; i < common.language.length; i++) { ?>
    <li><a href="<?= urlBasePath + webconfig.routes[routeKey.split('_')[0] + '_' + common.language[i].code].url ?>"><?- common.language[i].name ?></a></li>
    <? } ?>
</ul>

Custom Template Engine

It is possible to let the Express Template Engine implementation to bypass the NodeAtlas Template Engine implementation for view render. To do this, use the engine parameter. See an example with the Handlebars engine:

First, add the Express Handlebars middleware amongs your modules:

npm install express-handlebars

then, use engine with the arbtrary hbs value

{
    "engine": "hbs",
    "controller": "common.js",
    "variation": "common.json",
    "routes": {
        "/": {
            "view": "index.hbs",
            "variation": "index.json"
        }
    }
}

and explain to Express from NodeAtlas how to render views:

exports.setModules = function () {
    var NA = this;

    NA.modules.exphbs = require("express-handlebars");
};

exports.setConfigurations = function (next) {
  var NA = this,
    exphbs = NA.modules.exphbs;

    NA.express.engine("hbs", exphbs());

    next();
};

finaly, see what could be the content of index.hbs:

<!DOCTYPE html>
<html lang="en-us">
    <head>
        <meta charset="utf-8">
        <title>{{specific.titlePage}}</title>
        <link rel="stylesheet" href="stylesheets/{{common.classCssCommon}}.css" media="all">
        <link rel="stylesheet" href="stylesheets/{{specific.classPage}}.css" media="all">
    </head>
    <body class="{{specific.classPage}}">
        <div>
            <h1>{{specific.titlePage}}</h1>
            {{{specific.content}}}
        </div>
        <script async="true" type="text/javascript" src="javascripts/{{common.classJsCommon}}.js"></script>
    </body>
</html>

The goal of engine, it is to not use the NodeAtlas Template Engine but use it from Express. Because Express need a response object to render view, it is not possible to use this feature with the NA.view function from NodeAtlas API. NA.view only support, EJS, Pug and NodeAtlas syntaxes.

Differences between engine, templateEngineDelimiter and pug

It's possible to render EJS and Pug view with the Express Template Engine. In this case, because node-atlas use already ejs and pug modules as dependencies, it is not mandatory to use a controller and a npm command to set them. You have just to set engine: "ejs" or engine: "pug".

However, do this remove all additional feature added by NodeAtlas for this engines like for example the dynamic include of view for Pug in the common view file with #{routeParameters.view}.

No view

It is possible to not using a view and only use a controller. In this case, the changeVariations hook is unused. You will fill the locals.dom value yourself with the changeDom hook.

webconfig.json

{
    "routes": {
        "/(:member/)?": {
            "controller": "index.js",
            "mimeType": "application/json"
        }
    }
}

controllers/index.js

exports.changeDom = function (next, locals) {
    locals.dom = `{
  "params": ${locals.params.member},
  "query": ${locals.query.member},
  "body": ${locals.body.member}
}`;

    next();
};

So to the http://localhost/huey/?query=dewey URL requested in POST with member=louie body you will have the ouput:

{
  "params": "huey",
  "query": "dewey",
  "body": "louie"
}

No routes

No webconfig example not use the routes parameter. But it is also optional than others. For example, with the following webconfig:

webconfig.json

{
    "controller": "common.js"
}

and the following controller:

controllers/common.js

exports.setRoutes = function (next) {
    var NA = this,
        route = NA.webconfig.routes = {};

    route["/"] = {
        "mimeType": "text/plain"
    };

    next();
};

exports.changeDom = function (next, locals) {

    locals.dom = "Hello World";

    next();
};

It is possible to have at the address http://localhost/ a simple "Hello World" message.

Cache

It's a good thing to not serve file with no modification in production. You could set the websconfig's cache option to true for this:

{
    "cache": true,
    "route": {
        "/": "index.htm"
    }
}

You can also start node-atlas with --cache option :

node-atlas --cache

or set your environment variable NODE_ENV to production :

if you are in Unix / MacOS

export NODE_ENV=production

or if you are in Windows

SET NODE_ENV=production

or you can run NodeAtlas like this:

NODE_ENV=production node-atlas

or you can also set it in your JavaScript file:

process.env.NODE_ENV = "production";

SQL Database

We will see now how to use data from the database. We will use MySQL for this example. The mysql npm module will be useful. And first, install a MySQL server.

So, from your webconfig.json directory, use:

npm install mysql

MySQL Database

First, we will create a database demo on the server:

CREATE DATABASE demo;

and select it:

USE demo

and create a user table:

CREATE TABLE user
(
    id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
    lastname VARCHAR(100),
    firstname VARCHAR(100),
    email VARCHAR(255),
    birthdate DATE,
    gender TINYINT(1),
    country VARCHAR(255),
    town VARCHAR(255),
    zipcode VARCHAR(5),
    address VARCHAR(255)
);

and fill it with this set of data:

INSERT INTO user (
    lastname,
    firstname,
    email,
    birthdate,
    gender,
    country,
    town,
    zipcode,
    address
) VALUES (
    "Elric",
    "Edward",
    "edward.elric@fma.br",
    "2006/01/01",
    true,
    "Amestris",
    "Resembool",
    00000,
    "The Elric's house"
);
INSERT INTO user (
    lastname,
    firstname,
    email,
    birthdate,
    gender,
    country,
    town,
    zipcode,
    address
) VALUES (
    "Elric",
    "Alphonse",
    "alphonse.elric@fma.br",
    "2008/01/01",
    true,
    "Amestris",
    "Resembool",
    00000,
    "The Elric's house"
);

NodeAtlas Files

See now what files we will create to present our example:

├─ controllers/
│  ├─ common.js
│  └─ index.js
├─ models/
│  ├─ objects/
│  │  └─ user.js
│  └─ connectors/
│     └─ user.js
├─ views/
│  └─ index.htm
├─ variations/
│  ├─ common.json
│  └─ index.json
└─ webconfig.json

We will use the following webconfig.json with the custom _mysqlConfig variable which contains all information for database connection:

{
    "controller": "common.js",
    "variation": "common.json",
    "statics": {
        "/models": "models/objects"
    },
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "index.json",
            "controller": "index.js"
        }
    },
    "_mysqlConfig": {
        "host": "localhost",
        "user": "root",
        "password": "root",
        "database": "demo"
    }
}

Then, we will be connected to the database with the common controller controllers/common.js:

exports.setModules = function () {
    var NA = this;

    // Import of `mysql` module.
    NA.modules.mysql = require('mysql');

    // Create a model collection...
    NA.models = {};
    // ...and use the User model with MySQL connection capability.
    NA.models.User = require('../models/connectors/user.js');
};

exports.setConfigurations = function (next) {
    var NA = this,
        path = NA.modules.path,
        mysql = NA.modules.mysql;

    // Create a connection pool to MySQL.
    NA.mySql = mysql.createPool(NA.webconfig._mysqlConfig);

    next();
};

And display result via specific controller controllers/index.js:

exports.changeVariations = function (next, locals) {
    var NA = this,
        user = new NA.models.User(),
        user2 = new NA.models.User(),
        user3 = new NA.models.User(),
        user4 = new NA.models.User();

    NA.mySql.getConnection(function(err, connection) {
        if (err) {
            throw err;
        }

        // Read example.
        user
        .setConnection(connection)
        .lastname("Elric")
        .read(function (allUsers) {
            locals.user = user;
            locals.users = allUsers;

            // Create Example.
            user2
            .setConnection(connection)
            .firstname("Winry")
            .lastname("Rockbell")
            .email("winry.rockbell@fma.br")
            .gender(true)
            .create(function (infos) {
                locals.insertId = infos.insertId;
                locals.user2 = user2;

                // Update Example.
                user3
                .gender(false)
                .birthdate("2008-01-01")
                .country("Amestris")
                .town("Resembool")
                .zipcode("99999")
                .address("The Rockbell's house");

                user2.update(user3, function (infos) {
                    locals.affectedRows = infos.affectedRows;
                    locals.user2 = user2;

                    // Delete Example.
                    user4
                    .setConnection(connection)
                    .gender(false)
                    .delete(function (infos) {
                        locals.deletedRows = infos.affectedRows;
                        next();
                    });
                });
            });
        });
    });
};

with the user model via connecting file to database models/connectors/user.js:

var user = require('../objects/user.js');

function User(connection) {
    var privates = {},
        publics = this;

    user.call(publics);

    privates.connection = connection;

    publics.setConnection = function (connection) {
        privates.connection = connection;
        return publics;
    };

    publics.read = function (callback) {
        var select = `SELECT
                    id,
                    lastname,
                    firstname,
                    email,
                    birthdate,
                    gender,
                    country,
                    town,
                    zipcode,
                    address
                FROM user`,
            where = "";

        if (publics.id()) { where += ' && `id` = ' + publics.id(); }
        if (publics.lastname()) { where += ' && `lastname` = "' + publics.lastname() + '"'; }
        if (publics.firstname()) { where += ' && `firstname` = "' + publics.firstname() + '"'; }
        if (publics.email()) { where += ' && `email` = "' + publics.email() + '"'; }
        if (publics.birthdate()) { where += ' && `birthdate` = "' + publics.birthdate() + '"'; }
        if (typeof publics.gender() === "boolean") { where += ' && `gender` = ' + (publics.gender() ? 1 : 0); }
        if (publics.country()) { where += ' && `country` = "' + publics.country() + '"'; }
        if (publics.town()) { where += ' && `town` = "' + publics.town() + '"'; }
        if (publics.zipcode()) { where += ' && `zipcode` = "' + publics.zipcode() + '"'; }
        if (publics.address()) { where += ' && `address` = "' + publics.address() + '"'; }

        where = where.replace("&&", "WHERE");

        privates.connection.query(select + where, function (err, rows) {
            var users = [],
                user;

            if (err) {
                throw err;
            }

            if (rows[0]) {
                publics.id(rows[0].id);
                publics.lastname(rows[0].lastname);
                publics.firstname(rows[0].firstname);
                publics.email(rows[0].email);
                publics.birthdate(rows[0].birthdate);
                publics.gender((rows[0].gender) ? true : false);
                publics.country(rows[0].country);
                publics.town(rows[0].town);
                publics.zipcode(rows[0].zipcode);
                publics.address(rows[0].address);
            }

            for (var i = 0; i < rows.length; i++) {
                user = new User();
                user.id(rows[i].id);
                user.lastname(rows[i].lastname);
                user.firstname(rows[i].firstname);
                user.email(rows[i].email);
                user.birthdate(rows[i].birthdate);
                user.gender((rows[i].gender) ? true : false);
                user.country(rows[i].country);
                user.town(rows[i].town);
                user.zipcode(rows[i].zipcode);
                user.address(rows[i].address);
                users.push(user);
            }

            if (callback) {
                callback(users);
            }
        });

        return publics;
    };

    publics.create = function (callback) {
        var insert = "INSERT INTO user (",
            values = ") VALUES (";

        if (publics.id()) {
            insert += "`id`, ";
            values += publics.id() + ', ';
        }
        if (publics.lastname()) {
            insert += "`lastname`, ";
            values += '"' + publics.lastname() + '", ';
        }
        if (publics.firstname()) {
            insert += "`firstname`, ";
            values += '"' + publics.firstname() + '", ';
        }
        if (publics.email()) {
            insert += "`email`, ";
            values += '"' + publics.email() + '", ';
        }
        if (publics.birthdate()) {
            insert += "`birthdate`, ";
            values += '"' + publics.birthdate() + '", ';
        }
        if (typeof publics.gender() === "boolean") {
            insert += "`gender`, ";
            values += (publics.gender() ? 1 : 0) + ', ';
        }
        if (publics.country()) {
            insert += "`country`, ";
            values += '"' + publics.country() + '", ';
        }
        if (publics.town()) {
            insert += "`town`, ";
            values += '"' + publics.town() + '", ';
        }
        if (publics.zipcode()) {
            insert += "`zipcode`, ";
            values += '"' + publics.zipcode() + '", ';
        }
        if (publics.address()) {
            insert += "`address`, ";
            values += '"' + publics.address() + '", ';
        }

        insert = insert.replace(/, $/g, "");
        values = values.replace(/, $/g, ")");

        privates.connection.query(insert + values, function (err, infos) {
            if (err) {
                throw err;
            }

            publics.id(infos.insertId);

            if (callback) {
                callback(infos);
            }
        });

        return publics;
    };

    publics.update = function (user, callback) {
        var update = "UPDATE user SET",
            where = "";

        if (user.id()) { update += '`id` = ' + user.id() + ', '; }
        if (user.lastname()) { update += '`lastname` = "' + user.lastname() + '", '; }
        if (user.firstname()) { update += '`firstname` = "' + user.firstname() + '", '; }
        if (user.email()) { update += '`email` = "' + user.email() + '", '; }
        if (user.birthdate()) { update += '`birthdate` = "' + user.birthdate() + '", '; }
        if (typeof user.gender() === "boolean") { update += '`gender` = ' + (user.gender() ? 1 : 0) + ', '; }
        if (user.country()) { update += '`country` = "' + user.country() + '", '; }
        if (user.town()) { update += '`town` = "' + user.town() + '", '; }
        if (user.zipcode()) { update += '`zipcode` = "' + user.zipcode() + '", '; }
        if (user.address()) { update += '`address` = "' + user.address() + '", '; }

        update = update.replace(/, $/g, "");

        if (publics.id()) { where += ' && `id` = ' + publics.id(); }
        if (publics.lastname()) { where += ' && `lastname` = "' + publics.lastname() + '"'; }
        if (publics.firstname()) { where += ' && `firstname` = "' + publics.firstname() + '"'; }
        if (publics.email()) { where += ' && `email` = "' + publics.email() + '"'; }
        if (publics.birthdate()) { where += ' && `birthdate` = "' + publics.birthdate() + '"'; }
        if (typeof publics.gender() === "boolean") { where += ' && `gender` = ' + (publics.gender() ? 1 : 0); }
        if (publics.country()) { where += ' && `country` = "' + publics.country() + '"'; }
        if (publics.town()) { where += ' && `town` = "' + publics.town() + '"'; }
        if (publics.zipcode()) { where += ' && `zipcode` = "' + publics.zipcode() + '"'; }
        if (publics.address()) { where += ' && `address` = "' + publics.address() + '"'; }

        where = where.replace("&&", "WHERE");

        privates.connection.query(update + where, function (err, infos) {
            if (err) {
                throw err;
            }

            if (user.id()) { publics.id(user.id()); }
            if (user.lastname()) { publics.lastname(user.lastname()); }
            if (user.firstname()) { publics.firstname(user.firstname()); }
            if (user.email()) { publics.email(user.email()); }
            if (user.birthdate()) { publics.birthdate(user.birthdate()); }
            if (typeof publics.gender() === "boolean") { publics.gender(user.gender()); }
            if (user.country()) { publics.country(user.country()); }
            if (user.town()) { publics.town(user.town()); }
            if (user.zipcode()) { publics.zipcode(user.zipcode()); }
            if (user.address()) { publics.address(user.address()); }

            if (callback) {
                callback(infos);
            }
        });

        return publics;
    };

    publics.delete = function (callback) {
        var del = "DELETE FROM user",
            where = "";

        if (publics.id()) { where += ' && `id` = ' + publics.id(); }
        if (publics.lastname()) { where += ' && `lastname` = "' + publics.lastname() + '"'; }
        if (publics.firstname()) { where += ' && `firstname` = "' + publics.firstname() + '"'; }
        if (publics.email()) { where += ' && `email` = "' + publics.email() + '"'; }
        if (publics.birthdate()) { where += ' && `birthdate` = "' + publics.birthdate() + '"'; }
        if (typeof publics.gender() === "boolean") { where += ' && `gender` = ' + (publics.gender() ? 1 : 0); }
        if (publics.country()) { where += ' && `country` = "' + publics.country() + '"'; }
        if (publics.town()) { where += ' && `town` = "' + publics.town() + '"'; }
        if (publics.zipcode()) { where += ' && `zipcode` = "' + publics.zipcode() + '"'; }
        if (publics.address()) { where += ' && `address` = "' + publics.address() + '"'; }

        where = where.replace("&&", "WHERE");

        privates.connection.query(del + where, function (err, infos) {
            if (err) {
                throw err;
            }

            if (publics.id()) { publics.id(undefined); }
            if (publics.lastname()) { publics.lastname(undefined); }
            if (publics.firstname()) { publics.firstname(undefined); }
            if (publics.email()) { publics.email(undefined); }
            if (publics.birthdate()) { publics.birthdate(undefined); }
            if (typeof publics.gender() === "boolean") { publics.gender(undefined); }
            if (publics.country()) { publics.country(undefined); }
            if (publics.town()) { publics.town(undefined); }
            if (publics.zipcode()) { publics.zipcode(undefined); }
            if (publics.address()) { publics.address(undefined); }

            if (callback) {
                callback(infos);
            }
        });

        return publics;
    };
}

User.prototype = Object.create(user.prototype);
User.prototype.constructor = User;

module.exports = User;

based on user class shared between client-side and server-side models/objects/user.js:

(function (expose, factory) {
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = factory;
    } else {
        expose.User = factory;
    }
}(this, function User() {
    var privates = {},
        publics = this;

    publics.id = function (id) {
        if (typeof id === 'undefined') {
            return privates.id;
        } else {
            privates.id = id;
            return publics;
        }
    };

    publics.lastname = function (lastname) {
        if (typeof lastname === 'undefined') {
            return privates.lastname;
        } else {
            privates.lastname = lastname;
            return publics;
        }
    };

    publics.firstname = function (firstname) {
        if (typeof firstname === 'undefined') {
            return privates.firstname;
        } else {
            privates.firstname = firstname;
            return publics;
        }
    };

    publics.email = function (email) {
        if (typeof email === 'undefined') {
            return privates.email;
        } else {
            privates.email = email;
            return publics;
        }
    };

    publics.birthdate = function (birthdate) {
        if (typeof birthdate === 'undefined') {
            return privates.birthdate;
        } else {
            privates.birthdate = birthdate;
            return publics;
        }
    };

    publics.gender = function (gender) {
        if (typeof gender === 'undefined') {
            return privates.gender;
        } else {
            privates.gender = gender;
            return publics;
        }
    };

    publics.country = function (country) {
        if (typeof country === 'undefined') {
            return privates.country;
        } else {
            privates.country = country;
            return publics;
        }
    };

    publics.town = function (town) {
        if (typeof town === 'undefined') {
            return privates.town;
        } else {
            privates.town = town;
            return publics;
        }
    };

    publics.zipcode = function (zipcode) {
        if (typeof zipcode === 'undefined') {
            return privates.zipcode;
        } else {
            privates.zipcode = zipcode;
            return publics;
        }
    };

    publics.address = function (address) {
        if (typeof address === 'undefined') {
            return privates.address;
        } else {
            privates.address = address;
            return publics;
        }
    };
}));

With following files to display the page:

views/index.htm

<!DOCTYPE html>
<html lang="en-us">
    <head>
        <meta charset="utf-8" />
        <title><?- common.titleWebsite ?></title>
    </head>
    <body>
        <div class="title"><?- common.titleWebsite ?></div>
        <div>
            <h1><?- specific.titlePage ?></h1>
            <div class="first">
                <?- specific.content ?>
                <ul>
                    <li>Id: <strong><?- user.id() ?></strong></li>
                    <li>Lastname: <strong><?- user.lastname() ?></strong></li>
                    <li>Firstname: <strong><?- user.firstname() ?></strong></li>
                    <li>Email: <strong><?- user.email() ?></strong></li>
                    <li>Birthdate: <strong><?- user.birthdate() ?></strong></li>
                    <li>Gender: <strong><?- user.gender() ?></strong></li>
                    <li>Country: <strong><?- user.country() ?></strong></li>
                    <li>Town: <strong><?- user.town() ?></strong></li>
                    <li>Zipcode: <strong><?- user.zipcode() ?></strong></li>
                    <li>Address: <strong><?- user.address() ?></strong></li>
                </ul>
            </div>
            <div class="all">
                <?- specific.contents ?>
                <? for (var i = 0; i < users.length; i++) { ?>
                <ul>
                    <li>Id: <strong><?- users[i].id() ?></strong></li>
                    <li>Lastname: <strong><?- users[i].lastname() ?></strong></li>
                    <li>Firstname: <strong><?- users[i].firstname() ?></strong></li>
                    <li>Email: <strong><?- users[i].email() ?></strong></li>
                    <li>Birthdate: <strong><?- users[i].birthdate() ?></strong></li>
                    <li>Gender: <strong><?- users[i].gender() ?></strong></li>
                    <li>Country: <strong><?- users[i].country() ?></strong></li>
                    <li>Town: <strong><?- users[i].town() ?></strong></li>
                    <li>Zipcode: <strong><?- users[i].zipcode() ?></strong></li>
                    <li>Address: <strong><?- users[i].address() ?></strong></li>
                </ul>
                <? } ?>
            </div>
            <div class="last">
                <?- specific.contentInsert ?>
                <p>insertId: <?- insertId ?></p>
                <p>numberUpdate: <?- affectedRows ?></p>
                <ul>
                    <li>Id: <strong><?- user2.id() ?></strong></li>
                    <li>Lastname: <strong><?- user2.lastname() ?></strong></li>
                    <li>Firstname: <strong><?- user2.firstname() ?></strong></li>
                    <li>Email: <strong><?- user2.email() ?></strong></li>
                    <li>Birthdate: <strong><?- user2.birthdate() ?></strong></li>
                    <li>Gender: <strong><?- user2.gender() ?></strong></li>
                    <li>Country: <strong><?- user2.country() ?></strong></li>
                    <li>Town: <strong><?- user2.town() ?></strong></li>
                    <li>Zipcode: <strong><?- user2.zipcode() ?></strong></li>
                    <li>Address: <strong><?- user2.address() ?></strong></li>
                </ul>
                <p>numberDelete: <?- deletedRows ?></p>
            </div>
        </div>
    </body>
</html>

variations/common.json

{
    "titleWebsite": "Example MySql",
    "male": "Man",
    "female": "Woman"
}

variations/index.json

{
    "titlePage": "User Table",
    "content": "<p>First entry details.</p>",
    "contents": "<p>All entries details.</p>",
    "contentInsert": "<p>Added and Updated user details.</p>"
}

You will get the following output:

<!DOCTYPE html>
<html lang="en-us">
    <head>
        <meta charset="utf-8" />
        <title>MySql Exemple</title>
    </head>
    <body>
        <div class="title">MySql Exemple</div>
        <div>
            <h1>Table User</h1>
            <div class="first">
                <p>Détail de la première entrée.</p>
                <ul>
                    <li>Id: <strong>1</strong></li>
                    <li>Lastname: <strong>Elric</strong></li>
                    <li>Firstname: <strong>Edward</strong></li>
                    <li>Email: <strong>edward.elric@fma.br</strong></li>
                    <li>Birthdate: <strong>Sun Jan 01 2006 00:00:00 GMT+0100 (Paris, Madrid)</strong></li>
                    <li>Gender: <strong>true</strong></li>
                    <li>Country: <strong>Amestris</strong></li>
                    <li>Town: <strong>Resembool</strong></li>
                    <li>Zipcode: <strong>0</strong></li>
                    <li>Address: <strong>The Elric's house</strong></li>
                </ul>
            </div>
            <div class="all">
                <p>Détail de toutes les entrées.</p>
                <ul>
                    <li>Id: <strong>1</strong></li>
                    <li>Lastname: <strong>Elric</strong></li>
                    <li>Firstname: <strong>Edward</strong></li>
                    <li>Email: <strong>edward.elric@fma.br</strong></li>
                    <li>Birthdate: <strong>Sun Jan 01 2006 00:00:00 GMT+0100 (Paris, Madrid)</strong></li>
                    <li>Gender: <strong>true</strong></li>
                    <li>Country: <strong>Amestris</strong></li>
                    <li>Town: <strong>Resembool</strong></li>
                    <li>Zipcode: <strong>0</strong></li>
                    <li>Address: <strong>The Elric's house</strong></li>
                </ul>
                <ul>
                    <li>Id: <strong>2</strong></li>
                    <li>Lastname: <strong>Elric</strong></li>
                    <li>Firstname: <strong>Alphonse</strong></li>
                    <li>Email: <strong>alphonse.elric@fma.br</strong></li>
                    <li>Birthdate: <strong>Tue Jan 01 2008 00:00:00 GMT+0100 (Paris, Madrid)</strong></li>
                    <li>Gender: <strong>true</strong></li>
                    <li>Country: <strong>Amestris</strong></li>
                    <li>Town: <strong>Resembool</strong></li>
                    <li>Zipcode: <strong>0</strong></li>
                    <li>Address: <strong>The Elric's house</strong></li>
                </ul>
            </div>
            <div class="last">
                <p>Détail de l'utilisateur ajouté puis modifié.</p>
                <p>insertId: 3</p>
                <p>numberUpdate: 1</p>
                <ul>
                    <li>Id: <strong>3</strong></li>
                    <li>Lastname: <strong>Rockbell</strong></li>
                    <li>Firstname: <strong>Winry</strong></li>
                    <li>Email: <strong>winry.rockbell@fma.br</strong></li>
                    <li>Birthdate: <strong>2008-01-01</strong></li>
                    <li>Gender: <strong>false</strong></li>
                    <li>Country: <strong>Amestris</strong></li>
                    <li>Town: <strong>Resembool</strong></li>
                    <li>Zipcode: <strong>99999</strong></li>
                    <li>Address: <strong>The Rockbell's house</strong></li>
                </ul>
                <p>numberDelete: 1</p>
            </div>
        </div>
    </body>
</html>

NoSQL Database

We will see now how to use data from NoSQL database. We will use the mongoose npm module. And first, install a MongoDB server.

So, from your webconfig.json directory, use:

npm install mongoose

MongoDB Database

First, we will create a database demo on the server and select it:

use demo

and create a user collection:

db.createCollection("user")

and fill it with this document:

db.user.insert({
    email: "john.doe@unknown.com",
    identity: {
        lastname: "Doe",
        firstname: "John",
        gender: true,
        birthdate : new Date("1970/01/01")
    },
    location: {
        country: "Unknown",
        town: "Unknown",
        zipcode: "00000",
        address: "42 unknown"
    }
})

NodeAtlas Files

With the following data set:

├─ controllers/
│  ├─ common.js
│  └─ index.js
├─ models/
│  └─ user.js
├─ views/
│  └─ index.htm
├─ variations/
│  ├─ common.json
│  └─ index.json
└─ webconfig.json

We will use the following webconfig.json with the custom _mongodbConfig variable which contain all informations for database connection:

{
    "controller": "common.js",
    "variation": "common.json",
    "statics": {
        "/models": "models"
    },
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "index.json",
            "controller": "index.js"
        }
    },
    "_mongodbConfig": {
        "host": "localhost",
        "port": "27017",
        "database": "demo"
    }
}

With following files to display the page:

views/index.htm

<!DOCTYPE html>
<html lang="<?- languageCode ?>">
    <head>
        <meta charset="utf-8" />
        <title><?- common.titleWebsite ?></title>
    </head>
    <body>
        <div class="title"><?- common.titleWebsite ?></div>
        <div>
            <h1><?- specific.titlePage ?></h1>
            <?- specific.content ?>
            <ul>
                <li>Id: <strong><?- id ?></strong></li>
                <li>Lastname: <strong><?- lastname ?></strong></li>
                <li>Firstname: <strong><?- firstname ?></strong></li>
                <li>Email: <strong><?- email ?></strong></li>
                <li>Birthdate: <strong><?- birthdate ?></strong></li>
                <li>Gender: <strong><?- gender ?></strong></li>
                <li>Country: <strong><?- country ?></strong></li>
                <li>Town: <strong><?- town ?></strong></li>
                <li>Zipcode: <strong><?- zipcode ?></strong></li>
                <li>Address: <strong><?- address ?></strong></li>
            </ul>
        </div>
    </body>
</html>

variations/common.json

{
    "titleWebsite": "Example MongoDB",
    "male": "Man",
    "female": "Woman"
}

variations/index.json

{
    "titlePage": "User Collection",
    "content": "<p>Document `{ \"identity.firstname\": \"John\" }` details.</p>"
}

And last, we will be connected to the database with the common controller controllers/common.js:

exports.setModules = function () {
    var NA = this,
        path = NA.modules.path;

    NA.modules.mongoose = require('mongoose');
    NA.models = {};
    NA.models.User = require('../models/user.js');
};

exports.setConfigurations = function (next) {
    var NA = this,
        mongoose = NA.modules.mongoose,
        config = NA.webconfig._mongodbConfig;

    mongoose.Promise = global.Promise;
    mongoose.model("user", NA.models.User, "user");
    mongoose.connect("mongodb://" + config.host + ":" + config.port + "/" + config.database, function (error) {
        next();
    });
};

And display result via specific controller controllers/index.js:

exports.changeVariations = function (next, locals) {
    var NA = this,
        mongoose = NA.modules.mongoose,
        User = mongoose.model('user');

    User
    .findOne({ "identity.firstname": "Bruno" })
    .exec(function (err, user) {

        locals.id = user._id;
        locals.lastname = user.identity.lastname;
        locals.firstname = user.identity.firstname;
        locals.birthdate = user.identity.birthdate;
        locals.email = user.email;
        locals.gender = (user.identity.gender) ? locals.common.male : locals.common.female;
        locals.country = user.location.country;
        locals.town = user.location.town;
        locals.zipcode = user.location.zipcode;
        locals.address = user.location.address;

        next();
    });
};

based on user classe shared between client-side and server-side part models/user.js:

var mongoose;
if (typeof module !== 'undefined' && module.exports) {
     mongoose = require('mongoose');
}

(function (expose, factory) {
    if (mongoose) {
        module.exports = factory;
    } else {
        expose.User = factory;
    }
}(this, new mongoose.Schema({
    _id: mongoose.Schema.Types.ObjectId,
    email: { type : String, match: /^\S+@\S+$/ },
    identity: {
        lastname: String,
        firstname: String,
        gender: Boolean,
        birthdate : { type : Date, default : Date.now }
    },
    location: {
        country: String,
        town: String,
        zipcode: String,
        address: String
    }
})));

You will get the following output:

<!DOCTYPE html>
<html lang="en-us">
    <head>
        <meta charset="utf-8" />
        <title>MongoDB Example</title>
    </head>
    <body>
        <div class="title">MongoDB Example</div>
        <div>
            <h1>User Collection</h1>
            <p>Collection `{ "identity.firstname": "Bruno" }` details.</p>
            <ul>
                <li>Id: <strong>5804d4d530788ee2e52ea1c7</strong></li>
                <li>Lastname: <strong>Doe</strong></li>
                <li>Firstname: <strong>John</strong></li>
                <li>Email: <strong>john.doe@unknown.com</strong></li>
                <li>Birthdate: <strong>Mon Jan 01 1970 00:00:00 GMT+0200 (Paris, Madrid (heure d’été))</strong></li>
                <li>Gender: <strong>Homme</strong></li>
                <li>Country: <strong>Unknown</strong></li>
                <li>Town: <strong>Unknown</strong></li>
                <li>Zipcode: <strong>00000</strong></li>
                <li>Address: <strong>42 unknown</strong></li>
            </ul>
        </div>
    </body>
</html>

Isomorphic App

An isomorphic app is an app which JavaScript source code is for a big part the same as client-side executed code and as server-side executed code. NodeAtlas provide an example of an isomorphic app in the template dedicated to Vue.js.

For test this, just:

create a test folder:

mkdir hello-vue
cd hello-vue

then place it the hello-vue content

node-atlas --create hello-vue

then install dependencies

npm install

and finaly run the french version

node-atlas --browse

or the international version

node-atlas --browse --webconfig webconfig.en-us.json

You will find all you need about server-side code from constrollers/common.js and client-side code on https://ssr.vuejs.org/ and from assets/javascripts/common.js on https://vuejs.org/.