Server.js

/**
 * Import vendor modules
 * @ignore
 */
const os = require('os');
const express = require('express');

/**
 * Import own modules
 * @ignore
 */
const Logger = require('./Logger');
const Config = require('./Config');
const Router = require('./Router');
const Controller = require('./Controller');

class Server {
    /**
     * Setup an internal express app
     *
     * @access private
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @type {Express}
     */
    #app = express();

    /**
     * Internal bind hostname reference for Express
     *
     * @access private
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @type {string}
     */
    #host;

    /**
     * Internal bind port reference for Express
     *
     * @access private
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @type {number}
     */
    #port;

    /**
     * Server class
     *
     * @class Server
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @see https://www.npmjs.com/package/express
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     * const Api = require('./routers/Api');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.includeDefaultBodyParsers();
     *     server.loadRouters([Api]);
     *     server.run();
     * });
     */
    constructor() {
        // Log server start
        Logger.info('[SERVER] App starting...');

        // Set the host and port
        this.#host = Config.application.host;
        this.#port = Config.application.port;

        // Set the Express app to allow proxy's
        this.#app.enable('trust proxy');

        // Disable powered by header for security reasons
        this.#app.disable('x-powered-by');

        // Handle Google Cloud Loadbalancer requests (If the app try's to redirect from /)
        this.#app.use((req, res, next) => {
            if (req.get('User-Agent') === 'GoogleHC/1.0') {
                return res.send('Hello Google');
            }

            next();
        });

        // Expose library headers
        this.#app.use((req, res, next) => {
            const packageInformation = require(__dirname + '/../package.json');
            res.set('X-Neo-Beach', 'true');
            res.set('X-Neo-Beach-Core', packageInformation.version);

            next();
        });

        // Expose a health check
        this.#app.use((req, res, next) => {
            if(req.originalUrl === '/_health') {
                const packageInformation = require(__dirname + '/../package.json');

                res.json({
                    status: 'UP',
                    host: os.hostname(),
                    core: packageInformation.version,
                    load: process.cpuUsage(),
                    mem: process.memoryUsage(),
                    uptime: process.uptime()
                });

                return;
            }

            next();
        });
    }

    /**
     * Starts the Express server
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @returns {function}
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     * const Api = require('./routers/Api');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.run();
     * });
     */
    run() {
        return this.#app.listen(this.#port, this.#host, () => {
            Logger.info(`[SERVER] Service started with success! App running at: ${this.#host}:${this.#port}`)
        });
    }

    /**
     * Load global middlewares into the Express app
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @see http://expressjs.com/en/guide/writing-middleware.html
     *
     * @param {array.<function(*, *, *)>} middlewares - An array with Express middleware functions
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *    server.loadMiddlewares([
     *        (req, res, next) => {
     *            // Execute custom code here
     *            next();
     *        }
     *    ]);
     *    server.run();
     * });
     */
    loadMiddlewares(middlewares) {
        middlewares.forEach(mw => {
            this.#app.use(mw);
        });

        Logger.info(`[SERVER] Loaded ${middlewares.length} global middleware(s)`);
    }

    /**
     * Load routers into the Express app
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @param {array} routers - An array with Routers
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     * const Api = require('./routers/Api');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.loadRouters([Api]);
     *     server.run();
     * });
     */
    loadRouters(routers) {
        Logger.info(`[SERVER] Loaded ${routers.length} router(s)`);

        routers.forEach(router => {
            if(router instanceof Router) {
                // Get Routes from Router
                const routes = router.routes;

                // Check if we have routes available
                if(routes.length === 0) {
                    Logger.warn(`[ROUTER] ${router.name} is initialized without routes!`);
                }

                // Loop over routes to initialize controllers
                routes.forEach(route => {
                    // Check if the controller extends our base controller
                    if(route.controller instanceof Controller) {
                        // Add all middlewares for every route
                        for (const mw of route.middlewares) {
                            this.#app.use(route.path, mw);
                        }

                        // Add the router from the controller
                        this.#app.use(route.path, route.controller.getRouter(router.name));
                    } else {
                        console.error('Error at line:', route);
                        throw new Error(`Class is not an instance of '@neobeach/core/Controller'!`);
                    }
                });

                Logger.info(`[SERVER] ${router.name}: Loaded ${routes.length} controller(s)`);
            } else {
                console.error('Error at line:', router);
                throw new Error(`Class is not an instance of '@neobeach/core/Router'!`);
            }
        });
    }

    /**
     * Serves a static directory from the Express app
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @param {string} directory - Local directory path where the static files live
     * @param {string} [prefix] - Optional prefix to create a virtual path
     *
     * @see https://expressjs.com/en/starter/static-files.html
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.loadStatic('public');
     *     server.run();
     * });
     */
    loadStatic(directory, prefix = '/') {
        if(prefix === '/') {
            this.#app.use(express.static(directory));
        } else {
            this.#app.use(prefix, express.static(directory));
        }

        Logger.info(`[SERVER] Serving static files on / from: ${directory}`);
    }

    /**
     * Sets an Express app parameter
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @param {string} name - An express parameter name
     * @param {*} value - You specified value for the parameter
     *
     * @see https://expressjs.com/en/4x/api.html#app.set
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.setParameter('title', 'My Site');
     *     server.run();
     * });
     */
    setParameter(name, value) {
        this.#app.set(name, value);

        Logger.info(`[SERVER] Setting the express parameter ${name}: ${JSON.stringify(value)}`);
    }

    /**
     * Sets the Express render engine to EJS
     *
     * @access public
     * @since 2.1.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @param {string} views - Path to the EJS views directory
     *
     * @see https://expressjs.com/en/guide/using-template-engines.html
     * @see https://ejs.co/
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.setEJSViewEngine(`${__dirname}/views`);
     *     server.run();
     * });
     */
    setEJSViewEngine(views) {
        this.#app.set('view engine', 'ejs');
        this.#app.set('views', views);

        Logger.info(`[SERVER] Enabling EJS as render engine, views directory: ${views}`);
    }

    /**
     * Includes/loads default express body parsers (json, text, urlencoded and multer) with recommended config into the Express app
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @see https://expressjs.com/en/api.html#express.json
     * @see https://expressjs.com/en/api.html#express.text
     * @see https://expressjs.com/en/api.html#express.urlencoded
     * @see https://github.com/expressjs/multer
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.includeDefaultBodyParsers();
     *     server.run();
     * });
     */
    includeDefaultBodyParsers() {
        const multer = require('multer');

        this.#app.use(express.json());
        this.#app.use(express.text());
        this.#app.use(express.urlencoded({extended: false}));
        this.#app.use(multer().none());

        Logger.info(`[SERVER] Loaded default body parsers (json, text, urlencoded and multer)`);
    }

    /**
     * Includes/loads default security headers with recommended config into the Express app
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @see https://www.npmjs.com/package/helmet
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.includeDefaultSecurityHeaders();
     *     server.run();
     * });
     */
    includeDefaultSecurityHeaders() {
        const helmet = require('helmet');

        this.#app.use(helmet());

        Logger.info(`[SERVER] Loaded default security headers`);
    }

    /**
     * Includes/loads default CORS headers with recommended config into the Express app
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @see https://www.npmjs.com/package/cors
     *
     * @param {String} origin - String with allowed origin URL's
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.includeDefaultCorsHeaders();
     *     server.run();
     * });
     */
    includeDefaultCorsHeaders(origin) {
        const cors = require('cors');

        this.#app.use(cors({
            origin
        }));

        Logger.info(`[SERVER] Loaded default CORS headers`);
    }

    /**
     * Includes/loads default compression (deflate, gzip) with recommended config into the Express app
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @see https://www.npmjs.com/package/compression
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.includeDefaultCompression();
     *     server.run();
     * });
     */
    includeDefaultCompression() {
        const compression = require('compression');

        this.#app.use(compression({
            threshold: 0
        }));

        Logger.info(`[SERVER] Loaded default compression (deflate, gzip)`);
        Logger.warn(`[SERVER] !!! Please note: We recommend you to disable compression on production environments. Loadbalancers and reverse proxies are 9/10 times faster at doing this job... !!!`);
    }

    /**
     * Includes/loads cookie parser with recommended config into the Express app
     *
     * @access public
     * @since 2.1.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @see https://github.com/expressjs/cookie-parser
     *
     * @example
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.includeDefaultCookieParser();
     *     server.run();
     * });
     */
    includeDefaultCookieParser() {
        const cookieParser = require('cookie-parser');

        this.#app.use(cookieParser());

        Logger.info(`[SERVER] Loaded default cookie parser`);
    }

    /**
     * Attach a Remix Framework build to our Express server
     *
     * @access public
     * @since 1.0.0
     * @author Glenn de Haan
     * @copyright MIT
     *
     * @see https://remix.run/docs/en/v1
     * @see https://remix.run/docs/en/v1/other-api/adapter
     *
     * @param {*} serverBuild - A Remix Server build
     *
     * @example
     * import * as serverBuild from "@remix-run/dev/server-build";
     *
     * const {Runtime, Server} = require('@neobeach/core');
     *
     * const server = new Server();
     *
     * Runtime(() => {
     *     server.loadRemixFramework(serverBuild);
     *     server.run();
     * });
     */
    loadRemixFramework(serverBuild) {
        const {createRequestHandler} = require('@remix-run/express');

        this.#app.use('/build', express.static(`${process.cwd()}/public/build`, { immutable: true, maxAge: '1y' }));
        this.#app.use(express.static(`${process.cwd()}/public/build`, { maxAge: '1h' }));
        this.#app.use(express.static(`${process.cwd()}/public`, { maxAge: '1h' }));

        this.#app.all('*', createRequestHandler({
            build: serverBuild,
            mode: process.env.NODE_ENV
        }));

        Logger.info(`[SERVER] Loaded Remix Server Build`);
    }
}

/**
 * Export the server class
 * @ignore
 */
module.exports = Server;