Stats

 Latest version : 3.1.3

 Last updated on 2019-01-01T02:07:26.817Z

 Keywords : server, http, https, backend, web server, async, await, promise, REST, middleware

 Downloads :

  • 110 in Last Month

 Links :

 Examples

¯\_(ツ)_/¯
No examples found for this package

 Readme

a1-server

Next-gen async/await style web server. No need of callback style programming.

All the basic features in one module, routing, static & dynamic pages, REST services and reverse-proxy.

Built-in logger, or use preferred loggers at any time with no code refactoring.

Install express/connect middleware or create your own plugins.

Upgrading version

check the release notes

Installation

npm install a1-server

1 min Tutorial

Creating your own server has never been easier. The default configuration is quite handy (port 8080, static files at folder /public and dynamic files at folder /app)

Startup file at /index.js or /main.js :

const server = require('a1-server')
server.start()

Dynamic page at /app/hello.js;

module.exports = { get }
function get() {
  return 'Now is '+ (new Date()).toTimeString()
}

Now start the server and browse to http://localhost:8080/hello

node index

30 min Tutorial (or less)

HINT: check demo application in the module (node_modules/a1-server/demo)

starting the server

This module returns a promise after started. The parameter returned is a node http server. Then you could use the node httpServer object to, for instance, attach a web socket.

// --index.js page--
const server = require('a1-server')
// select start type (3 options)
server.start()   // default
server.start(8081) // custom port
server.start({...}) // config options, port, routing, etc
server.start().then(httpServer => {}).catch(err => {}) // perform actions after starting

now open terminal and type:

cd yourApp
node index.js

Configuration

To create a configuration object just add the properties you want to change, and pass the object when calling server.start()

const configuration = {
  port: 80,
  rules: {
    '/': 'landing.html',
    '/intranet/*': '/private/'
  }
}

server.start(configuration)

Available options, and their default values:

let configuration = {
  ssl: {
    /*key: fs.readFileSync('~/webapp/server.key'),
    cert: fs.readFileSync('~/webapp/server.crt')
    */
  },
  serverName: '',
  welcomePage: 'index.html',
  port: '8080',
  staticFolder: 'public',
  dynamicFolder: 'app',
  rules: {
    '/': 'index.html'
  },
  externalBodyParser: false,/* built-in parser, if using external parsers (i.e: body-parser), set to true*/
  performance: false,/*disable some built-in plugins to allow more throughput. If max request/sec, set to true*/
  Logger: Logger
}

Routing

Routing allows to:

  • serve static resources and dynamically generated resources.
  • beautify any .html request by removing the extension in the url.
  • use plain .js files to process requests, or to create REST APIs.
  • reverse proxying requests to other servers you trust in.

Automatic routing (recommended-no config!)

  • if the request has an extension (.html, .js, .css, .png, ...), a static file is served. This file should be located at the 'public' directory.
  • if the request has not extension:
  • if name + ".html" exists, that static file is served.
  • otherwise, a js file is executed in the server, and the result is sent back.

Examples:

  • /index.html will serve /public/index.html
  • /index will serve /public/index.html, since it exists
  • /process will execute /app/process.js

Custom Routing

By adding rules to the configuration. See 'url-pattern' npm module.

Note: For the same resources, the stricter rules must be written before the general ones because the routing will go to the first rule matching the url

const rules = {
  '/': '/index.html', // root page
  '/governance(/\*)': 'http://server1:8081', // proxy to private server
  '/cars(/:id)': '', // REST service
  '/bikes(/:id)': '/other/', // another REST example
  '/machines/:id/search': 'search', // go to search service
  '/machines/:id/latest': 'latest', //go to latest service
  '/machines(/:id(/:date))': 'machines', // /machines /machines/abc or machines/abc/20201231
}
const configuration = { rules }
server.start(configuration)

Static files

  • drop the resources (html, js, css) into the public folder
  • request the resources as usual http://server/css/main.css
  • As a nice feature, the html files can also be requested without the extension (.html)

Dynamic files

  • create a .js file in the app folder
  • exports the http methods you want to process (get post put delete options)
  • implement the exported functions as promises or async functions (or normal functions if no I/O processing). The output of the function should be either a JSON object, a simple type (number, string), or a stream.

IMPORTANT: no callbacks!!!

module.exports = { options, get }

// normal (synchronous) function since no blocking code
function options(request, response, params) {
  return ['GET']
}

// async keyword to avoid blocking
async function get(request, response, params) {
  return database.get(params.id)
}

When the URL has a queryString, the params object is filled with these parameters.

Creating a REST API

The same as with normal dynamic files. The only difference is to add a rule in the server configuration to be able to extract the 'path' parameters.

Configuration:

const rules = {
  '/cars(/:id)': '', /* /app/cars.js*/
  '/bikes(/:id)': '/inventory/motorbikes' /* /app/inventory/motorbikes.js*/
}

REST service:

The params object is already filled as declared in the rule. If the URL has a queryString, these parameters are also added to the params (but they do not override the REST ones if same keys).

// file at /app/cars.js

const http = require('http')
module.exports = { get }

// emulate a database
var cars = {
  '1': { name: 'volvo', engine: '6V' },
  '2': { name: 'seat', engine: '4L' }
}

// in this case (no I/O code) async is not needed
// but in real world cases the methods should be asynchronous
async function get(request, response, params) {
  if (params.id) return await getItem(request, response, params)
  else return await list(request, response, params)
}

async function getItem(request, response, params) {
  const obj = cars[params.id]
  if (!obj) {
    response.statusCode = 404
    return http.STATUS_CODES[404] // optional, send status message
  }
  else return obj
}

//the same function, but simpler, throwing error instead of managing the response manually
async function getItem(request, response, params) {
  if (!cars[params.id]) throw(404)
  else return obj
}

async function list(request, response, params) {
  return Object.keys(cars)
}

Note: to use the delete method, and avoid eslint or typescript warnings, declare the method with your preferred name (remove(), _delete(), etc...) in the module.exports variable. E.g: module.exports = { get, post, put, delete: remove}

throw() vs response error

Log errors is ok. Sending stack traces to users is silly.

There are two ways of sending errors:

  • Using throw(number) or throw(error): Recommended. Easy to code and to reason about. If you want to send a response error throw the HTTP error code as a number or throw the Exception. Since unhandled exceptions also throw errors, for security reasons, the response text is not sent to the client (i.e: if error is ENOENT file /home/gina/server/doc/3377 you would send that in that machine there is a user "gina", and the file location of your server, etc). So by default, and for your safety, the real error is not sent unless you handle the exception.
async function getItem(request, response, params) {
  if (!cars[params.id]) throw(404)
  else return obj
}
  • Setting the status code and text in the response: You are responsible of sending useful info to the user or to the service client. But be careful not to send exception error texts from node core or modules (error.text = sentitive info).
async function getItem(request, response, params) {
  const obj = cars[params.id]
  if (!obj) {
    response.statusCode = 404
    return http.STATUS_CODES[404] // or: return 'the parameter "id" is not found' or return err.message and so on.
  }
  else return obj
}

POST, PUT, DELETE & PATCH methods

Unlike GET method, these ones can contain data (payload) in the request. The built-in feature for these methods is to get the payload and add it to the request.body parameter. The body type is always a String.

This allows fastest processing of the request, instead of parsing body data for unwanted requests. Besides, documentation keeps easier to understand and it allows flexible solutions. JSON parsing was enabled before but for real world projects you usually have to check things before parsing JSON (user authenticated, resource exists in the database, etc, and discard the request promptly, so in the end, best is to leave the developer when to parse the data).

You can disable the built-in feature and use third party plugins ( body-parser and others) for parsing the body. Set externalBodyParser == true in the configuration object.

Plugins

A plugin is a function to be executed before a request has been processed. Plugins can be useful to check if user is authenticated, to insert headers, to log every request to the server, and so on.

request -> is static file?
  |- yes -> send the file
  |- no -> executePlugins -> execute and send the dynamic file

Add plugins the same way as connect or express middleware. The plugins for these applications are also valid here (passport, morgan, cookie-parser, etc...).

For custom plugins, remember to add next() or next(err) at the end of the function.

// express-type plugin (middleware)
const morgan = require('morgan')
server.use(morgan('combined'))

// custom plugin
server.use( (req, res, next) => {
  console.log('middleware executed')
  next()
})

WebSockets

The simplest way is by using the ws module, already downloaded with the server.

const WebSocketServer = require('ws').Server

server.start(serverConfiguration)
  .then(httpServer => startWebsocket(httpServer))
  .catch(err => throw err)

  function startWebsocket(httpServer) {
    const wss = new WebSocketServer({ server: httpServer })
    wss.on('connection', ws => {
      ws.on('message', message => {
        ws.send('response from the server')
      })
    })
  }  

Logging

By default no logging module is required (for better performance), but if any of the most popular logging systems (winston, bunyan, log4js, etc...) is a requirement, that logging component can be added in the configuration object.

This way, a developer only needs to use the Logger class shipped with the server. If in the future, the real logger is replaced by a new one, no code changes are required, just set the new logger you want to use in the configuration object.

// STEP-1 configure the Logger to use when starting the server
const configuration = {
  Logger: require('winston')
}
server.start(configuration)

// STEP-2 use the standard logger (it behaves as a proxy for the real logger)
// in the js files
const Logger = require('a1-server').Logger
const logger = Logger.getLogger('your-logger-name')
// ...
logger.error(err) // logged by using winston
logger.info('hi')

In development time, the default logger is attached to the console, so use logging instead of console.* methods from the beginning. If you prefer to have no logger output in development mode (for instance, to test requests performance), configure the Logger to NoOutputLogger.

let Logger = require('a1-server').Logger
Logger.configure(Logger.NoOutputLogger) //no output

Tips on development

  • to reload automatically when files are saved, use nodemon or pm2 (pm2 start app.js --watch)

Tips on production

To get the maximum performance, there are several useful techniques:

  • to use all CPU cores, use the cluster API or pm2 (pm2 start app.js -i 0)
  • to serve static files, either put the /public folder directly in a nginx location or keep the /public folder into the application and add a rule in the nginx (location ~* \.(css|js|gif|jpe?g|png)$ { expires 168h; })
  • to hide servers and/or ports, use nginx as reverse-proxy (it's faster than the built-in proxy)
  • to scale horizontally, use nginx as a load balancer

Example of nginx config:

upstream project {
    #ip_hash to keep users talking to the same server.
    ip_hash ;
    #the multiple servers, each running pm2
    server 22.22.22.2:3000;
    server 22.22.22.3:3000;
    server 22.22.22.5:3000;
}

server {
    listen 80;

    location / {
        proxy_pass http://project;
    }
    location ~* \.(css|js|gif|jpe?g|png)$ {
        expires 168h;
    }
}

More info at:

https://www.reddit.com/r/node/comments/6uwbh2/iveusedpm2toscalemyappacrosscoresona/

https://www.nginx.com/blog/5-performance-tips-for-node-js-applications/

 Comments