UNPKG

express-server-app

Version:

A minimal opinionated set of tools to create Web and REST servers

175 lines (158 loc) 4.81 kB
const express = require('express'); const boom = require('@hapi/boom'); const cors = require('cors'); const helmet = require('helmet'); const pinoMiddleware = require('express-pino-logger'); const { ValidationError } = require('express-json-validator-middleware'); const log = require('./log'); const { parseCorsOriginWhitelist } = require('./helpers'); const enableCors = () => { const { CORS_ORIGIN_WHITELIST } = process.env; const origin = parseCorsOriginWhitelist(CORS_ORIGIN_WHITELIST); if (process.env.NODE_ENV === 'production' && CORS_ORIGIN_WHITELIST === undefined) { log().warn( 'CORS_ORIGIN_WHITELIST is not configured. ' + 'CORS requests are allowed from all origins by default.', ); } return cors({ origin }); }; exports.enableCors = enableCors; const forceHttps = (port = 443) => (req, res, next) => { if (process.env.NODE_ENV !== 'production' || req.secure) { next(); return; } const hostnamePort = port !== 443 ? `:${port}` : ''; res.redirect(301, `https://${req.headers.host}${hostnamePort}${req.originalUrl}`); }; exports.forceHttps = forceHttps; const formatValidationErrors = (validationErrors) => Object.entries(validationErrors) .reduce((acc, [reqProp, errors]) => { acc[reqProp] = errors.map((error) => { const formattedError = { ...error }; delete formattedError.message; if (!formattedError.dataPath) delete formattedError.dataPath; return formattedError; }); return acc; }, {}); const logMiddleware = ({ ignorePaths, logger, } = {}) => pinoMiddleware({ autoLogging: { ignorePaths: ignorePaths || ['/healthy'], }, logger: logger || log(), customLogLevel: (res, err) => { if (res.statusCode >= 400 && res.statusCode < 500) { return 'warn'; } if (res.statusCode >= 500 || err) { return 'error'; } return 'info'; }, serializers: { err: (err) => { const isErr4xx = err.raw.isBoom && err.raw.output.statusCode >= 400 && err.raw.output.statusCode < 500; return { type: err.type, message: err.message, stack: isErr4xx ? undefined : err.stack, validationErrors: err.raw.validationErrors ? formatValidationErrors(err.raw.validationErrors) : undefined, }; }, req: (req) => ({ id: req.id, ip: req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.remoteAddress, headers: { host: req.headers.host, origin: req.headers.origin, 'user-agent': req.headers['user-agent'], }, method: req.method, url: req.url, }), res: (res) => ({ headers: { 'access-control-allow-credentials': res.headers['access-control-allow-credentials'], 'access-control-allow-headers': res.headers['access-control-allow-headers'], 'access-control-allow-methods': res.headers['access-control-allow-methods'], 'access-control-allow-origin': res.headers['access-control-allow-origin'], 'x-robots-tag': res.headers['x-robots-tag'], 'content-type': res.headers['content-type'], }, statusCode: res.statusCode, }), }, }); exports.logMiddleware = logMiddleware; const handle404 = (req, res, next) => next(boom.notFound()); exports.handle404 = handle404; const handleErrors = (err, req, res, next) => { const { statusCode } = err; const boomErr = boom.boomify(err, { statusCode }); res.err = boomErr; // Default error handler // http://expressjs.com/en/guide/error-handling.html#the-default-error-handler if (res.headersSent) { next(boomErr); return; } res.status(boomErr.output.statusCode) .set(boomErr.output.headers) .json({ ...boomErr.output.payload, id: req.id, }); }; exports.handleErrors = handleErrors; const handleValidationErrors = (err, req, res, next) => { if (err instanceof ValidationError) { err = boom.boomify( err, { message: 'Validation Error', statusCode: 422, }, ); } next(err); }; exports.handleValidationErrors = handleValidationErrors; const healthy = (req, res) => res.set('content-type', 'application/json').send(true); exports.healthy = healthy; const root = (req, res) => res.set('content-type', 'text/plain').send('Hello!'); exports.root = root; const getInitialMiddlewares = ({ cors: corsMiddleware = enableCors(), helmet: helmetMiddleware = helmet(), forceHttps: forceHttpsMiddleware = exports.forceHttps(), json = express.json(), logger = logMiddleware(), urlencoded = express.urlencoded({ extended: true }), } = {}) => [ helmetMiddleware, forceHttpsMiddleware, corsMiddleware, logger, json, urlencoded, ].filter(Boolean); exports.getInitialMiddlewares = getInitialMiddlewares; const getApiFinalMiddlewares = ({ errors = handleErrors, notFound = handle404, validationErrors = handleValidationErrors, } = {}) => [ notFound, validationErrors, errors, ].filter(Boolean); exports.getApiFinalMiddlewares = getApiFinalMiddlewares;