@lykmapipo/express-common
Version:
minimal express app configuration
783 lines (738 loc) • 19.3 kB
JavaScript
'use strict';
const env = require('@lykmapipo/env');
const path = require('path');
const _ = require('lodash');
const uuid = require('uuid');
const common = require('@lykmapipo/common');
const express = require('@lykmapipo/express-request-extra');
const expressRouterExtra = require('@lykmapipo/express-router-extra');
const morgan = require('morgan');
const cors = require('cors');
const compression = require('compression');
const bodyParser = require('body-parser');
const methodOverride = require('method-override');
const serveFavicon = require('serve-favicon');
const helmet = require('helmet');
const mquery = require('express-mquery');
const respond = require('express-respond');
const logger = require('@lykmapipo/logger');
/**
* @constant
* @name NODE_ENV
* @type {String}
* @description setup and ensure process runtime environment
* @default development
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* const NODE_ENV = process.env.NODE_ENV
* //=> development
*
*/
process.env.NODE_ENV = env.getString('NODE_ENV', 'development');
/**
* @constant
* @name BASE_PATH
* @type {String}
* @description setup and ensure process BASE_PATH
* @default process.cwd()
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* const BASE_PATH = process.env.BASE_PATH
* //=> /home/...
*
*/
process.env.BASE_PATH = env.getString('BASE_PATH', process.cwd());
/**
* @constant
* @name APP_PATH
* @type {String}
* @description setup and ensure process APP_PATH
* @default process.cwd()
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* const APP_PATH = process.env.APP_PATH
* //=> /home/...
*
*/
process.env.APP_PATH = path.resolve(
process.env.BASE_PATH,
process.env.APP_PATH || ''
);
/**
* @function correlationId
* @name correlationId
* @description http middleware for ensure request id and correlation id are
* available on request and response headers
* @param {Request} request valid express request object
* @param {Response} response valid express response object
* @param {Function} next valid express next middlware to pass control to
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.15.0
* @version 0.1.0
* @static
* @public
* @example
*
* const { correlationId } = require('@lykmapipo/express-common');
* app.use(correlationId);
* //=> request.headers {'x-correlation-id': ..., 'x-request-id': ...}
* //=> response._headers {'x-correlation-id': ..., 'x-request-id': ...}
*
*/
const correlationId = (request, response, next) => {
// obtain passed request or correlation id
let requestId =
request.get('X-Request-Id') || request.get('X-Correlation-Id');
// ensure requestId
requestId = _.isEmpty(requestId) ? uuid.v1() : requestId;
// merge to request headers
request.headers = common.mergeObjects(
{
'x-correlation-id': requestId,
'x-request-id': requestId,
},
request.headers
);
// set response headers
response.set('X-Correlation-Id', requestId);
response.set('X-Request-Id', requestId);
// continue
next();
};
/**
* @name notFound
* @function notFound
* @description http middleware to handle un matched routes
* @param {Request} request valid express request object
* @param {Response} response valid express response object
* @param {Function} next valid express next middlware to pass control to
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.15.0
* @version 0.1.0
* @static
* @public
* @example
*
* const { notFound } = require('@lykmapipo/express-common');
* app.use(notFound);
*
*/
const notFound = (request, response, next) => {
// build not foound error
const error = new Error('Not Found');
error.status = 404;
// continue
next(error);
};
/**
* @name errorHandler
* @function errorHandler
* @description http middleware to handle errors
* @param {Request} request valid express request object
* @param {Response} response valid express response object
* @param {Function} next valid express next middlware to pass control to
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.15.0
* @version 0.1.0
* @static
* @public
* @example
*
* const { errorHandler } = require('@lykmapipo/express-common');
* app.use(errorHandler);
*
*/
// eslint-disable-next-line no-unused-vars
const errorHandler = (error, request, response, next) => {
// TODO log error using logger
// reply with error
response.error(error);
};
/**
* @name app
* @function app
* @description express app with sensible defaults applied
* @returns {Object} valid express application
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.11.0
* @version 0.1.0
* @public
* @example
*
* const { app, start } = require('@lykmapipo/express-common');
* app.get('/v1/users', (req, res, next) => { });
*
* start((error) => { ... });
* //=> curl --request GET --url /v1/users
*
*/
const app = express();
/**
* @const
* @name env
* @type {String}
* @default development
* @description set express application environmnent
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* process.env.NODE_ENV=development
*
* app.set('env', getString('NODE_ENV'));
* app.get('env')
* //=> development
*
*/
app.set('env', env.getString('NODE_ENV', 'development'));
/**
* @const
* @name port
* @type {Number}
* @default 5000
* @description set express application port
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* process.env.PORT=5000
*
* app.set('port', getNumber('PORT', 5000));
* app.get('port')
* //=> 5000
*
*/
app.set('port', env.getNumber('PORT', 5000));
/**
* @const
* @name trust proxy
* @type {Boolean}
* @default false
* @description indicates the app is behind a front-facing proxy.
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.16.0
* @version 0.1.0
* @example
*
* process.env.TRUST_PROXY=false
*
* app.set('trust proxy', getBoolean('TRUST_PROXY', false));
*
*/
app.set('trust proxy', env.getBoolean('TRUST_PROXY', false));
/**
* @descrition setup and use correlationId middleware
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.14.0
* @version 0.1.0
* @example
*
* app.use(correlationId);
*
*/
app.use(correlationId);
/**
* @description setup and use morgan request log middleware
* @see {@link https://github.com/expressjs/morgan}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* process.env.LOGGER_LOG_ENABLED=true
* process.env.LOGGER_LOG_HTTP_FORMAT=combined
*
* app.use(morgan(LOGGER_LOG_HTTP_FORMAT, { stream }));
*
*/
const LOG_ENABLED = env.getBoolean('LOGGER_LOG_ENABLED', false) && !env.isTest();
if (LOG_ENABLED) {
const LOG_FORMAT = env.getString('LOGGER_LOG_HTTP_FORMAT', 'combined');
app.use(morgan(LOG_FORMAT, { stream: logger.stream }));
}
/**
* @description setup and use cors middleware
* @see {@link https://github.com/expressjs/cors}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* app.use(cors());
*
*/
app.use(cors());
/**
* @description use compression middleware
* @see {@link https://github.com/expressjs/compression}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* app.use(compression());
*
*/
app.use(compression());
/**
* @description setup and use express.static middleware
* @see {@link http://expressjs.com/en/starter/static-files.html}
* @see {@link http://expressjs.com/en/4x/api.html#express.static}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* process.env.SERVER_STATIC=true
* process.env.SERVE_STATIC_PATH=public
*
* app.use(express.static(STATIC_PATH));
*
*/
const SERVE_STATIC = env.getBoolean('SERVE_STATIC', false);
let STATIC_PATH;
if (SERVE_STATIC) {
STATIC_PATH = env.getString('SERVE_STATIC_PATH', 'public');
STATIC_PATH = path.resolve(env.getString('BASE_PATH'), STATIC_PATH);
app.use(express.static(STATIC_PATH));
}
/**
* @description setup and use serve favicon middleware
* @see {@link https://github.com/expressjs/serve-favicon}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.9.0
* @example
*
* process.env.SERVE_FAVICON=true
*
* app.use(serveFavicon(FAVICON_PATH));
*
*/
const SERVE_FAVICON = env.getBoolean('SERVE_FAVICON', false);
if (SERVE_FAVICON) {
const FAVICON_PATH = path.resolve(STATIC_PATH, 'favicon.ico');
app.use(serveFavicon(FAVICON_PATH));
}
/**
* @description setup and use urlencoded body-parser middleware
* @description parse application/x-www-form-urlencoded bodies
* @see {@link https: //github.com/expressjs/body-parser}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* process.env.BODY_PARSER_LIMIT=2mb
*
* app.use(bodyParse.urlencoded({ ... }));
*
*/
const BODY_PARSER_LIMIT = env.getString('BODY_PARSER_LIMIT', '2mb');
app.use(
bodyParser.urlencoded({
extended: true,
limit: BODY_PARSER_LIMIT,
})
);
/**
* @description setup and use json body-parser middleware
* @see {@link https: //github.com/expressjs/body-parser}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* process.env.BODY_PARSER_JSON_TYPE=application/json
* process.env.BODY_PARSER_LIMIT=2mb
*
* app.use(bodyParser.json({ ... }));
*
*/
const BODY_PARSER_JSON_TYPE = env.getString(
'BODY_PARSER_JSON_TYPE',
'application/json'
);
app.use(
bodyParser.json({
type: BODY_PARSER_JSON_TYPE,
limit: BODY_PARSER_LIMIT,
})
);
/**
* @description setup and use method-override middleware
* @see {@link https://github.com/expressjs/method-override}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* app.use(methodOverride('X-HTTP-Method'));
* app.use(methodOverride('X-HTTP-Method-Override'));
* app.use(methodOverride('X-Method-Override'));
* app.use(methodOverride('_method'));
*
*/
app.use(methodOverride('X-HTTP-Method')); // Microsoft
app.use(methodOverride('X-HTTP-Method-Override')); // Google/GData
app.use(methodOverride('X-Method-Override')); // IBM
app.use(methodOverride('_method')); // query param
/**
* @description setup and use helmet middleware
* @see {@link https://github.com/helmetjs/helmet}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* process.env.HELMET_HSTS=false
*
* app.use(helmet());
*
*/
const HELMET_HSTS = env.getBoolean('HELMET_HSTS', false);
if (HELMET_HSTS) {
app.use(helmet());
} else {
app.use(helmet({ hsts: false }));
}
/**
* @description setup and use express-mquery middleware
* @see {@link https://github.com/lykmapipo/express-mquery}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* process.env.MQUERY_LIMIT=10
* process.env.MQUERY_MAX_LIMIT=50
*
* app.use(mquery({ limit: MQUERY_LIMIT, maxLimit: MQUERY_MAX_LIMIT }));
*
*/
const MQUERY_LIMIT = env.getNumber('MQUERY_LIMIT', 10);
const MQUERY_MAX_LIMIT = env.getNumber('MQUERY_MAX_LIMIT', 50);
app.use(mquery({ limit: MQUERY_LIMIT, maxLimit: MQUERY_MAX_LIMIT }));
/**
* @description setup and use express-respond middleware
* @see {@link https://github.com/lykmapipo/express-respond}
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @example
*
* app.use(respond);
*
*/
app.use(respond);
/**
* @name mount
* @function mount
* @description mount router(s) into application
* @param {...Router} routers valid set of express routers to mount on app
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @public
* @example
*
* const { mount, Router } = require('@lykmapipo/express-common');
*
* const router = new Router({ version: '1.0.0' });
* const app = mount(router);
* //=> { [EventEmitter: app] ... }
*
*/
const mount = (...routers) => expressRouterExtra.mountInto(app, ...routers);
/**
* @name start
* @function start
* @description start express app
* @param {Number} [port] valid port
* @param {Function} [listener] callback to invoke on listen
* @author lally elias <lallyelias87@mail.com>
* @since 0.1.0
* @version 0.1.0
* @public
* @example
*
* const { start } = require('@lykmapipo/express-common');
* const app = start((error) => { ... });
* //=> { [EventEmitter: app] ... }
*
*/
const start = (port, listener) => {
// ensure port
let copyOfport = env.getNumber('PORT', 5000);
copyOfport = _.isNumber(port) ? port : copyOfport;
// ensure listener
const wrappedListener = () => {
const cb = _.isFunction(port) ? port : listener;
if (cb) {
cb(null, process.env);
}
};
// handle notFound & error
app.use(notFound);
app.use(errorHandler);
app.listen(copyOfport, wrappedListener);
// return app for chaining
return app;
};
/**
* @name testApp
* @function testApp
* @description express app used for api testing
* @returns {Object} valid express application
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.11.0
* @version 0.1.0
* @public
* @example
*
* const { testApp } = require('@lykmapipo/express-common');
* request(testApp)
* .get(`/v1/users`)
* .set('Accept', /json/)
* .expect('Content-Type', /json/)
* .expect(200).end((error, response) => { ... });
*
*/
const testApp = () => {
// handle notFound & error
app.use(notFound);
app.use(errorHandler);
// return app fot testing
return app;
};
/**
* @name use
* @function use
* @description mounts specified middlewares at the specified path.
* @param {String} [path] path for which the middleware functions are invoked
* @param {...Function} middlewares valid middleware functions
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.17.0
* @version 0.1.0
* @public
* @example
*
* const { use } = require('@lykmapipo/express-common');
*
* use('/v1/users', (req, res, next) => {
* console.log('Time: %d', Date.now());
* next();
* });
*
*/
const use = (...middlewares) => app.use(...middlewares);
/**
* @name all
* @function all
* @description matches all HTTP verbs at the specified path.
* @param {String} path path for which the middleware functions are invoked
* @param {...Function} middlewares valid middleware functions
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.17.0
* @version 0.1.0
* @public
* @example
*
* const { all } = require('@lykmapipo/express-common');
*
* all('/v1/users', (req, res, next) => {
* console.log('Time: %d', Date.now());
* next();
* });
*
*/
const all = (path, ...middlewares) => app.all(path, ...middlewares);
/**
* @name get
* @function get
* @description handle HTTP GET requests at specified path with specified
* callback function(s).
* @param {String} [path] path for which the middleware functions are invoked
* @param {...Function} middlewares valid middleware functions
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.17.0
* @version 0.1.0
* @public
* @example
*
* const { get } = require('@lykmapipo/express-common');
*
* get('/v1/users', (req, res, next) => {
* res.ok({ ... })
* });
* //=> curl --request GET --url /v1/users
*
* get('/v1/users/:id', (req, res, next) => {
* res.ok({ ... })
* });
* //=> curl --request GET --url /v1/users/1
*
*/
const get = (path, ...middlewares) => app.get(path, ...middlewares);
/**
* @name post
* @function post
* @description handle HTTP POST requests at specified path with specified
* callback function(s).
* @param {String} [path] path for which the middleware functions are invoked
* @param {...Function} middlewares valid middleware functions
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.17.0
* @version 0.1.0
* @public
* @example
*
* const { post } = require('@lykmapipo/express-common');
*
* post('/v1/users', (req, res, next) => {
* res.ok({ ... })
* });
* //=> curl --request POST \
* --url /v1/users \
* --data '{
* "name": "John Doe"
* }'
*
*/
const post = (path, ...middlewares) => app.post(path, ...middlewares);
/**
* @name put
* @function put
* @description handle HTTP PUT requests at specified path with specified
* callback function(s).
* @param {String} [path] path for which the middleware functions are invoked
* @param {...Function} middlewares valid middleware functions
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.17.0
* @version 0.1.0
* @public
* @example
*
* const { put } = require('@lykmapipo/express-common');
*
* put('/v1/users/:id', (req, res, next) => {
* res.ok({ ... })
* });
* //=> curl --request PUT \
* --url /v1/users/1 \
* --data '{
* "name": "John Doe"
* }'
*
*/
const put = (path, ...middlewares) => app.put(path, ...middlewares);
/**
* @name patch
* @function patch
* @description handle HTTP PATCH requests at specified path with specified
* callback function(s).
* @param {String} [path] path for which the middleware functions are invoked
* @param {...Function} middlewares valid middleware functions
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.17.0
* @version 0.1.0
* @public
* @example
*
* const { patch } = require('@lykmapipo/express-common');
*
* patch('/v1/users/:id', (req, res, next) => {
* res.ok({ ... })
* });
* //=> curl --request PATCH \
* --url /v1/users/1 \
* --data '{
* "name": "John Doe"
* }'
*
*/
const patch = (path, ...middlewares) => app.patch(path, ...middlewares);
/**
* @name del
* @function del
* @description handle HTTP DELETE requests at specified path with specified
* callback function(s).
* @param {String} [path] path for which the middleware functions are invoked
* @param {...Function} middlewares valid middleware functions
* @author lally elias <lallyelias87@mail.com>
* @license MIT
* @since 0.17.0
* @version 0.1.0
* @public
* @example
*
* const { del } = require('@lykmapipo/express-common');
*
* del('/v1/users/:id', (req, res, next) => {
* res.ok({ ... })
* });
* //=> curl --request DELETE --url /v1/users/1
*
*/
const del = (path, ...middlewares) => app.delete(path, ...middlewares);
Object.defineProperty(exports, 'Router', {
enumerable: true,
get: function () {
return expressRouterExtra.Router;
}
});
exports.all = all;
exports.app = app;
exports.correlationId = correlationId;
exports.del = del;
exports.errorHandler = errorHandler;
exports.get = get;
exports.mount = mount;
exports.notFound = notFound;
exports.patch = patch;
exports.post = post;
exports.put = put;
exports.start = start;
exports.testApp = testApp;
exports.use = use;