Small, efficient, web server.
All the basic features in one module, routing, static & dynamic pages, REST services and reverse-proxy.
Built-in logger, or use external loggers at any time with no code refactoring.
Install express/connect middleware or create your own plugins.
Check the release notes for changes when updating the server.
npm install a1-server
The default configuration is port 8080, static files at folder /public
and dynamic files at folder /app
. The server can be used either on ESM projects or CJS projects.
- /app
- hello.js
- /public
- index.html
- index.js
- package.json
Startup file at /index.js or /main.js :
import * as server from 'a1-server'
await server.start()
Dynamic page at /app/hello.js;
export function get() {
return 'Now is '+ new Date()
}
Now start the server and browse to http://localhost:8080/hello
node index
Startup file at /index.js or /main.js :
require('a1-server').start().catch(console.error)
Dynamic page at /app/hello.js;
module.exports = { get }
function get() {
return 'Now is '+ new Date()
}
Now start the server and browse to http://localhost:8080/hello
node index
HINT: check demo application in the module (node_modules/a1-server/demo)
This module returns a promise after start(). 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--
import * as server from 'a1-server'
// select start type (different options)
server.start() // default 8080 port
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
- default port is 8080, no config required
- using the env variable PORT when starting
PORT=8081 node index
- or use the port as first argument
node index 8081
- or use the port property in the index.js code
server.start(8081)
To create a configuration object just add the properties you want to change, and pass the object when calling server.start()
const configuration = {
port: 3000,
rules: {
'/': 'landing.html',
'/intranet/*': '/private/'
}
}
server.start(configuration)
Available options, and their default values:
let configuration = {
ssl: {
/*key: '~/webapp/server.key',
cert: '~/webapp/server.crt'
*/
},
serverName: '',
welcomePage: 'index.html',
port: '8080',
staticFolder: 'public',
dynamicFolder: 'app',
rules: {
'/': 'index.html'
},
Logger: Logger,
debug:false
}
These variables are optional. Only for starting the app without modifying the code (config file, loggers, etc).
- PORT: override port
PORT=9000 node index
- DEBUG: trace http requests and other hidden messages
DEBUG=true node index
- PERFORMANCE: only for benchmarks and performance regressions detection
PERFORMANCE=true node index
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.
- if the request has any 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
Note: for REST services, automatic routing is also enabled to detect path parameters (and therefore no need to add routing rules in most of the cases). See in sub-section REST.
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 }
await server.start(configuration)
Note: external routing is only useful on development phase (for convenience) or when reusing static resources. For the rest of cases it is better to provide the external features in another server address and then add a rule for proxying the resource (e.g.:
'/governance(/*)': 'http://server1:8081'
or using Nginx to redirect).
Note2: this is the same behaviour as default Nginx configuration for symbolic links.
In order to reuse /public and /app resources from other locations without copying them, you can create a symbolic link to the folder of the external resource. Only the files inside that folder (so no way to access parent resources by tricking with ../../) are provided. Note also that this feature should be used only in few cases and as a general rule, for security, you should not add symbolic links in the server folders (public, app, ).
Example:
- server1/app/control folder has persons.js and machines.js
- server2/app/ has a symbolic link to control, so persons and machines can be executed
- 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)
- 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.
// normal (synchronous) function since no blocking code
export function options(request, response, params) {
return ['GET']
}
// async keyword to avoid blocking
export async function get(request, response, params) {
return await database.get(params.id)
}
When the URL has a queryString, the params
object is filled with these parameters.
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. Since zero-config routing is also enabled for REST services, you could create a rest service with no rule and read the rest parameters named p0, p1, etc. No decorators or dependency injection is required.
Thanks to automatic REST rule generation, no need to manual typing in most of the cases. Example: /a/b/c/d
results in file: app/a/b.js
and params:{p0:c, p1:d}
. This allows direct reusability of REST services either by dropping or by linking into the /app folder. This also allows to generate REST services in real-time.
Configuration: none (that's the key to reuse code without pain)
REST service:
// file /app/people.js
export function get(req, res, params) {
const name = params.p0
const consult = params.p1
return params
}
URL request:
http://localhost:8080/people/Harris/permissions where the service is /people/
Sometimes you need or prefer explicit handling of the REST parameters, in this case add a custom rule in the configuration object when starting the server
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
import { STATUS_CODES } from 'http'
export { get, post, put, remove} // note `remove` instead of delete, this is translated automatically to DELETE method
// 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 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) // also throw('404') and throw new Error('404')
else return obj
}
async function list(request, response, params) {
return Object.keys(cars)
}
async function post(request, response, params) {
const data = JSON.parse(request.body)
if (!data.id) data.id = new Date().valueOf()
db[data.id] = data
response.statusCode = 201
let location = request.url.endsWith('/')? request.url + data.id:request.url + '/' + data.id
response.setHeader('Location', location)
return data
}
async function put(...){...}
async function remove(...){...}
Note: since
delete
is already a JS keyword, you can implementremove()
method instead. Another option is to export any function name as an aliasexport { remove as delete }
.
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 = sensitive info).
async function getItem(request, response, params) {
const obj = cars[params.id]
if (!obj) {
response.statusCode = 404
return 'Item not found'
// return STATUS_CODES[404]
}
else return obj
}
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 is not applicable for files (see below). This is a trade-off between usability and performance. For small bodies it is handy just to call request.body instead of parsing the event, and for big forms or files, the content must be parsed manually. The default should be check authorization headers and so on before parsing a body (to reject content early), for small bodies it is useful to get them available, but for bigger ones, like files, it is better to force the application to process the request event.
Depending of body content-type to be sent:
http://server/?param1=value1 (application/x-www-form-urlencoded)
.request.body
is availableJSON or text in body (text/plain, application/json)
.request.body
is available as StringFile or keyValue form (multipart/form-data)
.request.body
not available. The body content must be parsed manually. See example indemo
folder.
You can disable the built-in feature and use third party plugins ( body-parser and others) for parsing the body. These plugins usually add the files
or body
properties in the request.
A plugin is a function to be executed before a request is processed. Plugins can be useful to check if user is authenticated, to insert headers, to log every request to the server, and so on. Plugins can also be executed after the response output is created but not sent to client yet. This case is also useful for adding custom headers to the response based on the output.
Plugins are executed when requesting dynamic content, not on static pages like html, css, etc (they are static!). In case any static resource should be processed, create it as a dynamic file, then process custom content as dynamic page or with a plugin. Note also that plugins can be slow because they are executed in sequential order. Once a plugin throws error, the request process will stop. In the long term, this is better, because it is easier to understand the list of steps just by looking the order of installation for the plugins.
There are two types of plugins:
- standard plugins. These plugins can be sync or async functions, and they can also be easily added to the before or to the after events.
- middleware plugins. You can create or attach Express-like middleware with the
server.use()
method. Useful for reusing well-known, battle-tested middleware (passport, morgan, etc.). For custom middleware, do not forget to add next() or next(err) at the end of the function.
import * as server from 'a1-server'
// standard sync plugin
server.addBefore((req, res) => console.log(req.url))
// standard async plugin
server.addBefore(async (req, res) => await checkAuthorization(req))
// standard plugin executed after output created but before sent to client
server.addAfter((req, res) => res.setHeader('X-verification', createToken(res)))
// express-type plugin (middleware)
const expressPlugin = require('expressPlugin')
server.use(expressPlugin())
// custom plugin in middleware format
server.use( (req, res, next) => {
console.log('middleware executed')
next()
})
The code below is a full example of starting an http server and a websocket server. For more details, see the ws module package.
import * as server from 'a1-server'
import { Server as WebSocketServer } from 'ws'
server.start()
.then(httpServer => startWebsocket(httpServer))
.catch(err => throw err)
function startWebsocket(httpServer) {
let wss = new WebSocketServer({ server: httpServer })
wss.on('connection', ws => {
console.log('client open')
ws.on('close', client => console.log('client closed'))
ws.on('message', msg => ws.send('websocket server received ' + msg))
ws.on('error', console.error)
ws.on('pong', client => console.log('client is alive'))
})
return wss
}
There is a default built-in Logger tracing all requests and error responses into the console, so no need to use other logging systems. Anyway, if another Logger is preferred (winston, bunyan, log4js, pino, etc...), this can be added into the configuration object before starting. This way, the log statements and the log imports do not need to be updated in the code when changing from one Logger to another.
// 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 your js files
import * as server from 'a1-server'
const Logger = server.Logger
const logger = Logger.getLogger('your-logger-name')
// ...
logger.error(err) // logged by using winston
logger.info('hi') // logged by using winston
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 another logger later, just add it into the config object.
import {start,Logger} from 'a1-server'
start({ Logger: Logger.NoOutputLogger })
- to reload automatically when files are saved, use nodemon or pm2 (
pm2 start app.js --watch
)
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/ive_used_pm2_to_scale_my_app_across_cores_on_a/
https://www.nginx.com/blog/5-performance-tips-for-node-js-applications/