Manual Reference Source Test

src/middleware/Routes.js

// Import the necessary modules.
// @flow
import bodyParser from 'body-parser'
import compress from 'compression'
import helmet from 'helmet'
import type {
  $Application,
  $Request,
  $Response,
  NextFunction
} from 'express'
import responseTime from 'response-time'
import { STATUS_CODES as statusMessages } from 'http'

import {
  ApiError,
  statusCodes
} from '../helpers'

/**
 * Class for setting up the Routes.
 * @type {Routes}
 */
export default class Routes {

  /**
   * Create a new Routes object.
   * @param {!PopApi} PopApi - The PopApi instance to bind the routes to.
   * @param {!Object} options - The options for the routes.
   * @param {!Express} options.app - The application instance to add middleware
   * and bind the routes to.
   * @param {?Array<Object>} options.controllers - The controllers to register.
   * @throws {TypeError} - 'app' are required options for the routes
   * middleware!
   */
  constructor(PopApi: any, {app, controllers}: Object): void {
    const { name: debugName } = this.constructor
    PopApi.debug(`Registering ${debugName} middleware with options: %o`, {
      controllers
    })

    if (!app) {
      throw new TypeError('\'app\' is a required option for the Routes middleware!')
    }

    this.setupRoutes(app, PopApi, controllers)
  }

  /**
   * Register the controllers found in the controllers directory.
   * @param {!Express} app - The application instance to register the routers
   * to.
   * @param {!PopApi} PopApi - The PopApi instance to bind the routes to.
   * @param {!Array<Object>} controllers - The controllers to register.
   * @returns {undefined}
   */
  registerControllers(
    app: $Application,
    PopApi: any,
    controllers: Array<Object>
  ): void {
    controllers.forEach(c => {
      const { Controller, args } = c
      const controller = new Controller(args)

      PopApi.debug(`Registering ${Controller.name} route controller`)
      controller.registerRoutes(app, PopApi)
    })
  }

  /**
   * Convert the thrown errors to an instance of ApiError.
   * @param {!Error} err - The caught error.
   * @param {!IncomingMessage} req - The incoming message request object.
   * @param {!ServerResponse} res - The server response object.
   * @param {!Function} next - The next function to move to the next
   * middleware.
   * @returns {ApiError} - The converted error.
   */
  convertErrors(
    err: Error,
    req: $Request,
    res: $Response,
    next: NextFunction
  ): mixed {
    if (!(err instanceof ApiError)) {
      const error = new ApiError({
        message: err.message
      })
      return next(error)
    }

    return next(err)
  }

  /**
   * Catch the 404 errors.
   * @param {!IncomingMessage} req - The incoming message request object.
   * @param {!ServerResponse} res - The server response object.
   * @param {!Function} next - The next function to move to the next
   * middleware.
   * @returns {ApiError} - A standard 404 error.
   */
  setNotFoundHandler(
    req: $Request,
    res: $Response,
    next: NextFunction
  ): mixed {
    const err = new ApiError({
      message: 'Api not found',
      status: statusCodes.NOT_FOUND
    })

    return next(err)
  }

  /**
   * Error handler middleware
   * @param {!ApiError} err - The caught error.
   * @param {!IncomingMessage} req - The incoming message request object.
   * @param {!ServerResponse} res - The server response object.
   * @param {!Function} next - The next function to move to the next
   * middleware.
   * @returns {Object} - The error object.
   */
  setErrorHandler(
    err: ApiError,
    req: $Request,
    res: $Response,
    next: NextFunction
  ): Object {
    const { status } = err
    const body: {
      [key: string]: string
    } = {
      message: err.isPublic
        ? err.message
        : `${status} ${statusMessages[status]}`
    }

    if (process.env.NODE_ENV === 'development') {
      body.stack = err.stack
    }

    res.setHeader('Content-Type', 'application/json')
    res.status(status)
    return res.send(body)
  }

  /**
   * Remove security sensitive headers.
   * @see https://github.com/shieldfy/API-Security-Checklist#output
   * @param {!IncomingMessage} req - The incoming message request object.
   * @param {!ServerResponse} res - The server response object.
   * @param {!Function} next - The next function to move to the next
   * middleware.
   * @returns {undefined}
   */
  removeServerHeader(
    req: $Request,
    res: $Response,
    next: NextFunction
  ): mixed {
    res.removeHeader('Server')
    return next()
  }

  /**
   * Hook method for setting up middleware pre setting up the routes.
   * @param {!Express} app - The application instance to add middleware to.
   * @returns {undefined}
   */
  preRoutes(app: $Application): void {
    // Enable parsing URL encoded bodies.
    app.use(bodyParser.urlencoded({
      extended: true
    }))

    // Enable parsing JSON bodies.
    app.use(bodyParser.json())

    // Enables compression of response bodies.
    app.use(compress({
      threshold: 1400,
      level: 4,
      memLevel: 3
    }))

    // Enable response time tracking for HTTP request.
    app.use(responseTime())

    // Set and remove the security sensitive headers.
    app.use(helmet())
    app.use(helmet.contentSecurityPolicy({
      directives: {
        defaultSrc: ['\'none\'']
      }
    }))
    app.use(this.removeServerHeader)
  }

  /**
   * Hook method for setting up middleware post setting up the routes.
   * @param {!Express} app - The application instance to add middleware to.
   * @returns {undefined}
   */
  postRoutes(app: $Application): void {
    // Convert the caught errors to the ApiError instance.
    app.use(this.convertErrors)

    // Set the default not found handling middleware.
    app.use(this.setNotFoundHandler)

    // Set the default error handling middleware.
    app.use(this.setErrorHandler)
  }

  /**
   * Setup the application service.
   * @param {!Express} app - The application instance to add middleware and
   * bind the routes to.
   * @param {!PopApi} PopApi - The PopApi instance to bind the routes to.
   * @param {?Array<Object>} [controllers] - The controllers to register.
   * @returns {undefined}
   */
  setupRoutes(
    app: $Application,
    PopApi?: any,
    controllers?: Array<Object>
  ): void {
    // Pre routes hook.
    this.preRoutes(app)

    // Enable HTTP request logging.
    if (PopApi && PopApi.httpLogger) {
      app.use(PopApi.httpLogger)
    }

    // Register the controllers.
    if (controllers) {
      this.registerControllers(app, PopApi, controllers)
    }

    // Post routes hook.
    this.postRoutes(app)
  }

}