Simple node framework (SNF)
SNF is a simple node-js framework that provides simple ways to use log, cache, database, session, redis, request scope and more.
- Quick Start
- Configuration
- Base Classes
- Log
- Database
- Redis
- Cache
- Session
- Authorization
- Server
- Route
- Plugins
- Config
- Request Scope
- Auditor
- Util
- Erros
- Test
Quick Start
The best way to get started with SNF is using create-snf-app
npx create-snf-app my-app -p 8091cd my-appnpm start
With database and redis enabled:
npx create-snf-app my-app --enable-database --enable-redis -p 8091cd my-appnpm start
Configuration
If you are using create-snf-app all SNF configuration is located at api/config/env.
There are 1 file per environment (default, testing, staging, production), so its possible to execute the application using an defined environment.
NODE_ENV=production node index
Base Classes
Base classes are the most used strategy in SNF. Your class can inherit the base class and have your features and log prefixes.
create-snf-app has many examples of base class use.
Base Class | Description |
---|---|
Base | Log and timer features, the this.log and this.timer objects will be ready to use |
BaseController | Log and timer features, the this.log and this.timer objects will be ready to use |
BaseService | Log and timer features, the this.log and this.timer objects will be ready to use |
BaseRepository | Log and timer features, the this.log and this.timer objects will be ready to use |
BaseRest | Rest features. this.fetch (node-fetch), this.responseHandler, this.log and this.timer objects will be ready to use |
BaseSoap | Soap features. this.fetch (node-fetch), this.soap (soap), this.xml_parser this.log and this.timer objects will be ready to use |
Loggable | Log features, the this.log object will be ready to use |
const BaseController = Base;const CustomerService = ; // sample controller { super module: 'My Sample Controller' // the module name will prefix all your logs ; } async { super; thislog; req; return ; } moduleexports = Controller;
Log
SNF has an smart log feature that will be turned on by default.
Log Configuration
The logs will be saved at logs folder at root of the application.
If you turn of debug SNF will not output logs at your console, by default we turn of debug logs in production environment.
SNF uses bunyan to write great logs, so we have a node to configure bunyan too.
"log":
Log on stdout
If you need to output bunyan logs to process.stdout, do this configuration
"log":
Log methods
thislog;thislog;thislog;thislog;thislog;
Prefixed logs
All the logs will be automaticaly prefixed with module name, so if you write a log at controller class it will be prefixed with "Controller Name =>". This way you can view your application flow.
DEBUG My Sample Controller => Loading customer [diogo]DEBUG Customer Service => Loading customer [diogo]DEBUG Customer Repository => Loading customer [diogo]
Request id in the log
SNF log will automaticaly attach your request-id in the log if you call super.activateRequestLog(req) in the first line of your controller method.
Hostname in the log
SNF attach your /etc/hostname in the log, this way you can discover witch machine is writing the logs
Ignore request and response logs in some routes
To ignore some route on request/response logs plugin, just add ignore attribute on configuration file.
"log":
Logs with objects
Its possible to write logs with an JSON object. SNF will stringfy and prettfy your object before write.
const people = name: 'Jhon' age: 35 ;thislog;
{"name":"Application","host":"agility","hostname":"agility","pid":11627,"level":20,"pretty":"{\"obj\": {\"age\": 35, \"name\": \"Jhon\"}}","msg":"My Sample Controller => This is only a log","time":"2019-03-27T19:23:34.229Z","v":0}
If you want to log an object in json format, just sent inside a "natural" property this way the object will not be stringfied.
const people = name: 'Jhon' age: 35 ;thislog;
{"name":"Application","host":"agility","hostname":"agility","pid":11765,"level":20,"natural":{"name":"Jhon","age":35},"msg":"My Sample Controller => This is only a log","time":"2019-03-27T19:25:36.431Z","v":0}
If you need log one object as natural mode and another as string on the same call, just send "natural" and "pretty" properties:
const people = name: 'Jhon' age: 35 ;thislog;
{"name":"Application","host":"agility","hostname":"agility","pid":12488,"level":20,"natural":{"name":"Jhon","age":35},"pretty":"{\"age\": 35, \"name\": \"Jhon\"}","msg":"My Sample Controller => This is only a log","time":"2019-03-27T19:35:01.323Z","v":0}
Database
SNF support multiple connections at, mongo, oracle and sql server.
You can disable database handler by removing the "db" node at configuration file, or just runing create-snf-app using the --disable-database option.
"db":
MongoDB
To enable mongodb just add mongodb configuration to config db node.
"db":
By the way, we use mongoose client and the options node is mongoose options
You can use the secong connection like database.connections.mongodb.second.
const database = Singleton;const connection = databaseconnectionsmongodbsecond || mongoose; // mongoose model configuration startconst schema = mongoose; const model = connection;// mongoose model configuration end thismodel;
Sql Server
To enable sqlserver just add sqlserver configuration to config db node nd install mssql package dependency.
npm i mssql
"db":
By the way, we use mssql client and the options node is mssql options
To use, just get the pool and do the queries.
Internally, each ConnectionPool instance is a separate pool of TDS connections. Once you create a new Request/Transaction/Prepared Statement, a new TDS connection is acquired from the pool and reserved for desired action. Once the action is complete, connection is released back to the pool.
For more use samples, please see mssql documentation
const BaseService = Base;const config database = Singleton; { super module: 'People Repository' ; thisconfig = config; thisdatabase = database; thispool = databaseconnectionssqlserverapplication; } async { const sql = `SELECT * FROM PEOPLE WHERE ID = @id`; const result = await thispool input'id' id ; return result; } moduleexports = PeopleRepository;
Oracle
To enable oracle add oracle configuration to config db node and install oracledb package dependency.
npm i oracledb
"db":
You you need the Oracle Instant Client.
Download the client and extract at ~/lib. After, create ~/lib/network/admin/sqlnet.ora file with DISABLE_OOB=ON configuration.
More informations about DISABLE_OOB at:
- https://oracle.github.io/node-oracledb/doc/api.html#-621-poolclose
- https://github.com/oracle/node-oracledb/issues/688
- https://www.oracle.com/technetwork/database/features/instant-client/ic-faq-094177.html#A5028
By the way, we use oracledb client and the options node is oracledb options
To use, just get the pool and do the queries. Here you MUST close the connection after use.
For more use samples, please see oracledb documentation
const BaseService = Base;const config database = Singleton; { super module: 'People Repository' ; thisconfig = config; thisdatabase = database; thispool = databaseconnectionsoracleapplication; } async { const connection = await thispool; try const sql = `SELECT * FROM PEOPLE WHERE ID = :id`; const result = await connection; return result; finally connection; } moduleexports = PeopleRepository;
OnDatabaseConnect
If you need execute an action after database has connected, you should use the OnDatabaseConnect event.
// index.jsconst database = Singleton; database { // your code...} database { // your code... } database { // your code... }
Redis
SNF support has a redis handler to simplify connection and use.
You can disable redis handler by removing the "redis" node at configuration file, or just runing create-snf-app using the --disable-redis option.
By the way, we use io-redis as redis client.
Basic configuration:
"redis": ,
Advanced configuration:
"redis":
Using:
const redis = Singleton; // save user age in redisconst ttl = 300000; // 300000 miliseconds = 5 minutesawait redis; // get user age from redisconst age = await redis; // remove user age from redisredis; // remove all application keys from redisredis;
Cache
SNF has a cache handler that uses redis to save responses on the cache.
By the way, you need active redis configuration to use this feature
The cache handler idea is to automaticaly retreive response cache if exists.
"cache":
When the cache feature is active (by default), SNF inject the req.cache object.
So, when you have a sucess at you controller logic save the response on cache like this:
const BaseController = Base; { super module: 'My Sample Controller' ; } { const message = 'Sample controller test'; res; // save response in the cache const ttl = 300000; reqcache; return ; } moduleexports = Controller;
After this the cache handler will save in your redis the key:
simple-node-framework:cache:my-application:get:api/sample-module
To automaticaly retreive your information, you need to configure the Cache.loadResponse middleware, so your controller will start get the information from the cache:
const ControllerFactory Cache = ;const route = Singleton;const server = ;const Controller = ; const full = route; // sample of cached routeserver;
If you set a headerKey at configuration, the cache handler will look for the defined key at the request header and use this key to compose the cache key. This way, you can have unique user caches.
"cache":
simple-node-framework:user-identifier:cache:my-application:get:api/sample-module
Session
SNF has a session handler that uses redis to save application session.
By the way, you need active redis configuration to use this feature
The session handler idea is to automaticaly retreive the session and append at req.session.
"session": ,
When turned on, you can manipulate session data:
const BaseController = Base; { super module: 'My Sample Account Controller' ; } { if reqparamsuser === 'some-user' && reqparamspassword === 'some-pass' // create the session reqsessiondataname = 'Diogo'; reqsession; res; res; return ; } { // destroy the session reqsession; res; return ; }
And the session will be created:
myapplication-session:some-user-identifier
You can change an existing session:
const BaseController = Base; { super module: 'My Sample Controller' ; } { // update the session reqsession; reqsessiondataname = 'Diogo Menezes'; reqsession; res; return ; }
Like cache, if you set headerKey at configuration file, SNF will look for defined key at the header os request and automatic load the right session.
"session":
const BaseController = Base; { super module: 'My Sample Account Controller' ; } { if reqparamsuser === 'some-user' && reqparamspassword === 'some-pass' // create the session reqsessiondataname = 'Diogo'; // the session is automaticaly be created with "x-identifier" in the key reqsession; res; res; return ; } { super module: 'My Sample Controller' ; } { // the session is automaticaly loaded if you send "x-identifier" at the header reqsessiondataname = 'Diogo Menezes'; reqsession; res; return ; }
Authorization
SNF authorization handler protect your API with Basic or Bearer methods.
Basic Authorization
"Basic" Hypertext Transfer Protocol (HTTP) authentication scheme, which transmits credentials as user-id/password pairs, encoded using Base64.
This scheme is not considered to be a secure method of user authentication unless used in conjunction with some external secure system such as TLS (Transport Layer Security, RFC5246), as the user-id and password are passed over the network as cleartext.
"authorization":
To protect your route with basic authorization you have to use the authorization middleware.
const ControllerFactory = ;const route authorization = Singleton;const server = ;const Controller = ;const full = route; server;
Your controller dont need to be changed, all the work is made by the middleware.
const BaseController = Base; { super module: 'My Sample Controller' ; } { res; return ; } moduleexports = Controller;
Now, when your API receive a request without an basic authorization header the middlware will return "401 Unauthorized".
If the API receive an invalid user and password will return "403 Forbidden".
If everything is ok the midleware will pass to the next() stef of the chain.
Bearer Authorization
The Bearer authentication scheme is intended primarily for server authentication using the WWW-Authenticate and Authorization HTTP headers.
OAuth enables clients to access protected resources by obtaining an access token, which is defined as "a string representing an access authorization issued to the client", rather than using the resource owner's credentials directly.
Tokens are issued to clients by an authorization server with the approval of the resource owner. The client uses the access token to access the protected resources hosted by the resource server. This OAuth access token is a bearer token.
TLS is mandatory to implement and use with this specification.
"authorization":
To protect your route with bearer authorization you have to use the authorization middleware.
const ControllerFactory = ;const route authorization = Singleton;const server = ;const Controller = ;const full = route; server;
Your controller dont need to be changed, all the work is made by the middleware.
const BaseController = Base; { super module: 'My Sample Controller' ; } { res; return ; } moduleexports = Controller;
Now, when your API receive a request without an baerer authorization header the middlware will return "401 Unauthorized".
If the API receive an invalid token will return "403 Forbidden".
If everything is ok the midleware will pass to the next() stef of the chain.
To generate a valid JWT token, you can use the createJWT helper method.
const BaseController = Base; { super module: 'My Sample Account Controller' ; } { const user = sample if user // create the JWT token to use as Bearer Token const token = authorization res; res; return ; }
Now you can send this JWT token at authorization header to all protected api calls of this user.
Auth0 Authorization
The Auth0 authentication scheme is intended primarily for server authentication using Auth0 platform using jwks validation.
When you stand the Authorization header with the prefix Auth0 this middleware will validate your token.
Authorization: Auth0 eyJ0eXAiOiJKV1QiLCJhbGciOiJS...
"authorization":
To protect your route with auth0 authorization you have to use the authorization middleware.
const ControllerFactory = ;const route authorization = Singleton;const server = ;const Controller = ;const full = route; server;
Your controller dont need to be changed, all the work is made by the middleware.
const BaseController = Base; { super module: 'My Sample Controller' ; } { res; return ; } moduleexports = Controller;
The middleware will check the auth0 token at https://${YOUR_DOMAIN}/.well-known/jwks.json
When your API receive a request without an auth0 authorization header the middlware will return "401 Unauthorized".
If the API receive an invalid token will return "403 Forbidden".
If everything is ok the midleware will pass to the next() stef of the chain.
Custom Authorization
Like almost everything in SNF framework, the authorization class can be overrided, so you can create your custom-authorization class and customize the authorization methods.
const Authorization = ; { super module: 'Custom Authorization' ; } // do the basic validation of credentials { const username password = reqauthorizationbasic; if requser = requsername; ; else ; } moduleexports = ;
Server
The server class is the most important class in SNF because there we configure all other plugins, middlewares and helpers.
By the way, we use restify as rest framework.
Custom server
ATENTION: Before create your custom server, plese take a look at base Server class https://github.com/diogolmenezes/simple-node-framework/blob/master/lib/server.js You have to be mutch careful if you want to override this class because some default behavors may stop work after this.
const Server = ; { super module: 'SNF Custom Server' ; } // You can override server methods :) { super; thislog; } // You can override server methods :) { super; thislog; } // .. you can override all other methods ... const customServer = ;const server = customServer; moduleexports = server baseServer: customServerbaseServer;
Custom Server after listen callback
To run custom code after server listen, you have to send an afterListenCallBack
const Server = ; { super module: 'SNF Custom Server' ; } const customServer = ;const server = customServer; moduleexports = server baseServer: customServerbaseServer;
Custom Server SSL HTTPS
To use HTTPS, send httpsOptions to configure method.
const Server = ;const fs = ; { super module: 'SNF Custom Server' ; } const customServer = ;const server = customServer; moduleexports = server baseServer: customServerbaseServer;
Apply SSL certificate to solve UNABLE_TO_VERIFY_LEAF_SIGNATURE
If you need to send an CA to soap or fetch API calls, just create at application root path an _ssl folder.
Inside this folder create another folder with your cert name (my_company) ant put leaf.crt, inter.crt and root.crt files.
After this you need to call ssl.applyCerts on your index.js file. This method will import all your certfiles to global request agent.
// index.jsconst ssl = Singleton;ssl;
To get the CRT files you needs to:
- Download the 3 ".cer" certificates on the website leaf, inter and root.
- Convert CER files in CRT files openssl x509 -inform der -in leaf.cer -out leaf.crt
Route
The rote class is responsible for import all module routes and provide helper methods.
The info method return information about the route. By default SNF uses the app.baseRoute configuration as the base route of your api, so a full route is the union of baseRoute + moduleName
const authorization route = Singleton;const routeFile = __filename; // ex: { baseRoute: '/api', module: 'customer', full: '/api/customer' }const info = route;
Plugins
SNF has some plugins to help you.
Origin Plugin
The origin plugin will force that the api requesters sends some identification headers.
Header | Description |
---|---|
x-origin-application | Witch application is calling the api Ex.: billing-application |
x-origin-channel | Witch chanhel is calling the api Ex.: web, mobile |
x-origin-device | Witch device is calling the api Ex.: app-ios, app-android |
You can turn if of in configuration.
"origin":
Request and Response Plugin
This plugin will automaticaly log all requests and responses. It will enabled by default and the only way to disable it is overriding the configureMiddlewares method at SNF Server Class.
Config
The configuration is responsible for importing the correct configuration file according to the environment (NODE_ENV).
All configuration files are located at api/config/env directory.
If you dont define an NODE_ENV, SNF will import the default env file.
$ NODE_ENV=staging node index $ NODE_ENV=testing node index $ NODE_ENV=production node index
Request Scope
Request Scope is a feature that creates an scope to share information between objects.
All SNF base classes have the scope feature, because, one of best uses of scope is to share information on request lifecicle.
By the way scope is a port os ScopeJs.
To get more scope examples go to ScopeJs documentation
const BaseController = Base; // sample controller { super module: 'My Sample Controller' // the module name will prefix all your logs ; thispeopleService = ; } async { // creating the scope and adding some information this; thispeopleService; // you can get the scope that service class added console; // this.scope // Object {_chain: Array(4), name: "diogo", age: 34} req; return ; } moduleexports = Controller;
const BaseController = Base; // sample controller { super module: 'People Service' ; } { // assing more information to the scope this; } moduleexports = ;
Auditor
The auditor module is used to log features in the project. To use, include this configuration at your config file.
"audit": ,
Make sure that you have a mongo connection established before audit (Database).
To audit, just call the method audit. This method will create the collection audit (if not exist) and save the record.
auditor;
The method's arguments contract are actor, origin, action, label, object, description and metadata. All that are optional because function call need to be passive (not affect the application flow).
Util
Errors
Custom Errors
First you have to create your custom error class and override defineCustomErrors method.
// custom-errors.jsconst ErrorHandler = ; { super module: 'Custom Errors' ; } { super; thisrestifyErrorsSampleError1 = thisrestifyErrors; thisrestifyErrorsSampleError2 = thisrestifyErrors; } moduleexports = ;
Than you can use you sampleError:
const customErrors = ; // you can use your custom error on next { return ; } // or you can throw the custom error { try throw customErrors; catch error thislog; return ; }
Test
SNF provides some test facilities especially if you are using SNF by an create-snf-app application.