Source

factory/NestBootstraper.ts

import {
  INestApplication,
  Logger,
  NestInterceptor,
  PipeTransform,
} from "@nestjs/common";
import {
  AuthorizationExceptionFilter,
  ConflictExceptionFilter,
  GlobalExceptionFilter,
  HttpExceptionFilter,
  NotFoundExceptionFilter,
  ValidationExceptionFilter,
} from "./exceptions";
import { SwaggerBuilder } from "./openapi";
import { CorsOptions } from "@nestjs/common/interfaces/external/cors-options.interface";
import { CorsError } from "./errors";

/**
 * @description
 * Defines all customizable parameters for Swagger setup.
 *
 * @summary
 * This interface allows developers to customize how Swagger UI is configured
 * within the NestJS application. It includes parameters for titles, paths,
 * color schemes, and asset paths to tailor the API documentation experience.
 *
 * @param {string} title - Title displayed in Swagger UI.
 * @param {string} description - Description shown below the title.
 * @param {string} version - API version displayed in the documentation.
 * @param {string} [path] - Optional path where Swagger will be available.
 * @param {boolean} [persistAuthorization] - Whether authorization tokens persist across reloads.
 * @param {string} [assetsPath] - Path to custom assets for Swagger UI.
 * @param {string} [topbarBgColor] - Custom background color for the Swagger top bar.
 * @param {string} [topbarIconPath] - Path to a custom icon displayed in the top bar.
 * @param {string} [faviconPath] - Path to a custom favicon.
 */
export interface SwaggerSetupOptions {
  title: string;
  description: string;
  version: string;
  path?: string;
  persistAuthorization?: boolean;
  assetsPath?: string;
  topbarBgColor?: string;
  topbarIconPath?: string;
  faviconPath?: string;
}

/**
 * @description
 * A fluent, static bootstrap class for initializing and configuring a NestJS application.
 *
 * @summary
 * The `NestBootstraper` class provides a chainable API for configuring
 * a NestJS application instance. It includes built-in methods for enabling
 * CORS, Helmet security, Swagger documentation, global pipes, filters,
 * interceptors, and starting the server.
 *
 * This class promotes consistency and reduces repetitive setup code
 * across multiple NestJS projects.
 *
 * @example
 * ```ts
 * import { NestFactory } from "@nestjs/core";
 * import { AppModule } from "./app.module";
 * import { MyLogger } from "./MyLogger";
 * import { NestBootstraper } from "@decaf-ts/for-nest";
 *
 * async function bootstrap() {
 *   const app = await NestFactory.create(AppModule);
 *
 *   await NestBootstraper
 *     .initialize(app)
 *     .enableLogger(new MyLogger())
 *     .enableCors(["http://localhost:4200"])
 *     .useHelmet()
 *     .setupSwagger({
 *       title: "OpenAPI by TradeMark™",
 *       description: "TradeMark™ API documentation",
 *       version: "1.0.0",
 *       path: "api",
 *       persistAuthorization: true,
 *       topbarBgColor: "#2C3E50",
 *       topbarIconPath: "/assets/logo.svg",
 *       faviconPath: "/assets/favicon.ico"
 *     })
 *     .useGlobalFilters()
 *     .useGlobalPipes(...)
 *     .useGlobalInterceptors(...)
 *     .start(3000);
 * }
 *
 * bootstrap();
 * ```
 * @class
 */
export class NestBootstraper {
  private static app: INestApplication;
  private static _logger: Logger;

  /**
   * @description
   * Returns the current logger instance, creating a default one if not set.
   *
   * @summary
   * Ensures that a valid `Logger` instance is always available
   * for logging bootstrap-related messages.
   *
   * @return {Logger} The active logger instance.
   */
  private static get logger(): Logger {
    if (!this._logger) {
      // fallback
      this._logger = new Logger("NestBootstrap");
    }
    return this._logger;
  }

  /**
   * @description
   * Initializes the bootstrapper with a given NestJS application.
   *
   * @summary
   * Binds the provided NestJS app instance to the bootstrapper, enabling
   * chained configuration methods.
   *
   * @param {INestApplication} app - The NestJS application instance to initialize.
   * @return {NestBootstraper} Returns the class for chaining configuration methods.
   */
  static initialize(app: INestApplication) {
    this.app = app;
    return this;
  }

  /**
   * @description
   * Enables or replaces the global logger for the NestJS application.
   *
   * @summary
   * If a custom logger is provided, it replaces the default logger. Otherwise,
   * a new logger named `"NestBootstrap"` is used. This logger is also registered
   * with the NestJS application.
   *
   * @param {Logger} [customLogger] - Optional custom logger instance.
   * @return {NestBootstraper} Returns the class for chaining.
   */
  static enableLogger(customLogger?: Logger) {
    this._logger = customLogger || new Logger("NestBootstrap");
    this.app.useLogger(this._logger);
    return this;
  }

  /**
   * @description
   * Enables Cross-Origin Resource Sharing (CORS) for the application.
   *
   * @summary
   * Allows defining either a wildcard origin (`"*"`) or a list of allowed origins.
   * Automatically accepts local development requests and those without origin headers.
   * Throws a `CorsError` for unauthorized origins.
   *
   * @param {'*' | string[]} [origins=[]] - List of allowed origins or `"*"` to allow all.
   * @param {string[]} [allowMethods=['GET', 'POST', 'PUT', 'DELETE']] - Allowed HTTP methods.
   * @return {NestBootstraper} Returns the class for chaining configuration.
   *
   */
  static enableCors(
    origins: "*" | string[] = [],
    allowMethods: string[] = ["GET", "POST", "PUT", "DELETE"]
  ) {
    const allowedOrigins =
      origins === "*" ? "*" : origins.map((o) => o.trim().toLowerCase());

    const corsOptions: CorsOptions = {
      origin: (origin, callback) => {
        // Allow request without origin...
        if (!origin) return callback(null, true);

        if (
          allowedOrigins === "*" ||
          (Array.isArray(allowedOrigins) &&
            allowedOrigins.includes(origin.toLowerCase()))
        ) {
          return callback(null, true);
        }

        callback(new CorsError(`Origin ${origin} not allowed`));
      },
      credentials: true,
      methods: allowMethods.join(","),
    };

    this.app.enableCors(corsOptions);
    return this;
  }

  /**
   * @description
   * Applies the Helmet middleware for enhanced security.
   *
   * @summary
   * Dynamically loads the `helmet` package if available and registers it
   * as middleware to improve HTTP header security. If not installed, logs a warning
   * and continues execution without throwing errors.
   *
   * @param {Record<string, any>} [options] - Optional configuration passed to Helmet.
   * @return {NestBootstraper} Returns the class for chaining configuration.
   */
  static useHelmet(options?: Record<string, any>) {
    try {
      // eslint-disable-next-line @typescript-eslint/no-require-imports
      const helmet = require("helmet"); // Dynamic import to avoid hard dependency
      this.app.use(helmet(options));
      this.logger.log("Helmet middleware enabled successfully.");
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
    } catch (e: any) {
      this.logger.warn("Helmet not installed. Skipping middleware.");
    }

    return this;
  }

  /**
   * @description
   * Configures and initializes Swagger UI for API documentation.
   *
   * @summary
   * Uses the `SwaggerBuilder` utility to configure API documentation
   * with detailed customization for title, version, paths, and colors.
   * Swagger is automatically exposed at the configured path.
   *
   * @param {SwaggerSetupOptions} options - Swagger configuration options.
   * @return {NestBootstraper} Returns the class for chaining configuration.
   */
  static setupSwagger(options: SwaggerSetupOptions) {
    const swagger = new SwaggerBuilder(this.app, {
      title: options.title,
      description: options.description,
      version: options.version,
      path: options.path || "api",
      persistAuthorization: options.persistAuthorization ?? true,
      assetsPath: options.assetsPath,
      faviconFilePath: options.faviconPath,
      topbarIconFilePath: options.topbarIconPath,
      topbarBgColor: options.topbarBgColor,
    });
    swagger.setupSwagger();
    return this;
  }

  /**
   * @description
   * Registers one or more global validation pipes.
   *
   * @summary
   * Enables request payload validation and transformation globally across
   * the entire NestJS application. Multiple pipes can be chained together
   * for modular input validation.
   *
   * @param {...PipeTransform[]} pipes - Pipe instances to register globally.
   * @return {NestBootstraper} Returns the class for chaining.
   */
  static useGlobalPipes(...pipes: PipeTransform[]) {
    if (pipes.length > 0) this.app.useGlobalPipes(...pipes);
    return this;
  }

  /**
   * @description
   * Registers one or more global exception filters.
   *
   * @summary
   * If no filters are provided, it automatically registers a default
   * set of standard exception filters for common error types like
   * `HttpException`, `ValidationException`, `ConflictException`, and others.
   *
   * @param {...ExceptionFilter[]} filters - Optional filters to apply globally.
   */
  static useGlobalFilters(...filters: any[]) {
    const defaultFilters = [
      new HttpExceptionFilter(),
      new ValidationExceptionFilter(),
      new NotFoundExceptionFilter(),
      new ConflictExceptionFilter(),
      new AuthorizationExceptionFilter(),
      new GlobalExceptionFilter(),
    ];

    this.app.useGlobalFilters(
      ...(filters.length > 0 ? filters : defaultFilters)
    );
    return this;
  }

  /**
   * @description
   * Registers global interceptors for request and response transformation.
   *
   * @summary
   * Interceptors allow advanced request/response manipulation such as
   * serialization, logging, or transformation. Multiple interceptors
   * can be added for modular configuration.
   *
   * @param {...NestInterceptor[]} interceptors - Interceptor instances to register.
   * @return {NestBootstraper} Returns the class for chaining configuration.
   */
  static useGlobalInterceptors(...interceptors: NestInterceptor[]) {
    if (interceptors.length > 0)
      this.app.useGlobalInterceptors(...interceptors);
    return this;
  }

  /**
   * @description
   * Starts the NestJS application and binds it to the given port and host.
   *
   * @summary
   * Listens on the specified port and optionally a host. Once started,
   * logs the application URL for easy access. The startup process resolves
   * once the application is successfully running.
   *
   * @param {number} [port=3000] - Port number to listen on.
   * @param {string} [host] - Optional host or IP address to bind to.
   * @param {boolean} [log=true] - Whether to log the application URL upon startup.
   * @return {Promise<void>} Resolves once the application starts successfully.
   */
  static async start(
    port: number = Number(process.env.PORT) || 3000,
    host: string | undefined = undefined,
    log: boolean = true
  ) {
    this.app.listen(port, host as any).then(async () => {
      if (log) {
        const url = await this.app.getUrl();
        this.logger.log(`🚀 Application is running at: ${url}`);
      }
    });
  }
}