Repository: nestjs/swagger
Pull request #2840 (2024-02-15 08:02:31)

feat: allow to disable Swagger UI
## PR Checklist
Please check if your PR fulfills the following requirements:

- [X] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [X] Tests for the changes have been added (for bug fixes / features)
- [X] Docs have been added / updated (for bug fixes / features) => https://github.com/nestjs/docs.nestjs.com/pull/2972


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [X] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: none

Currently when using `SwaggerModule.setup` it serves all files of Swagger UI and API definitions generated on the fly (JSON and YAML). It isn't possible for now to only serve API definitions.

I see two uses-cases for this feature:
- You want to serve multiple API definitions in your Nest application, so you might want to enable the UI only for the "default" API and disable it for other ones.
- You already have a Swagger UI served somewhere else in your infrastructure and just want to make it point the API definition of your Nest app. So the UI might be disabled to reduce noise.

## What is the new behavior?

This PR introduces a way to disable the Swagger UI to only generate API definitions.
A new flag `swaggerUiEnabled` is available in `SwaggerModule.setup` options.

## Does this PR introduce a breaking change?
- [ ] Yes
- [X] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->

## Other information

In this PR, I also improved some documentation and removed dead code 


Patch:
diff --git a/lib/interfaces/swagger-custom-options.interface.ts b/lib/interfaces/swagger-custom-options.interface.ts
index a28e6d3a..aa3c15c4 100644
--- a/lib/interfaces/swagger-custom-options.interface.ts
+++ b/lib/interfaces/swagger-custom-options.interface.ts
@@ -1,23 +1,107 @@
 import { SwaggerUiOptions } from './swagger-ui-options.interface';
-import { SwaggerDocumentOptions } from './swagger-document-options.interface';
 import { OpenAPIObject } from './open-api-spec.interface';
 
 export interface SwaggerCustomOptions {
+  /**
+   * If `true`, Swagger resources paths will be prefixed by the global prefix set through `setGlobalPrefix()`.
+   * Default: `false`.
+   * @see https://docs.nestjs.com/faq/global-prefix
+   */
   useGlobalPrefix?: boolean;
+
+  /**
+   * If `false`, only API definitions (JSON and YAML) will be served (on `/{path}-json` and `/{path}-yaml`).
+   * This is particularly useful if you are already hosting a Swagger UI somewhere else and just want to serve API definitions.
+   * Default: `true`.
+   */
+  swaggerUiEnabled?: boolean;
+
+  /**
+   * Url point the API definition to load in Swagger UI.
+   */
+  swaggerUrl?: string;
+
+  /**
+   * Path of the JSON API definition to serve.
+   * Default: `{{path}}-json`.
+   */
+  jsonDocumentUrl?: string;
+
+  /**
+   * Path of the YAML API definition to serve.
+   * Default: `{{path}}-json`.
+   */
+  yamlDocumentUrl?: string;
+
+  /**
+   * Hook allowing to alter the OpenAPI document before being served.
+   * It's called after the document is generated and before it is served as JSON & YAML.
+   */
+  patchDocumentOnRequest?: <TRequest = any, TResponse = any>(
+    req: TRequest,
+    res: TResponse,
+    document: OpenAPIObject
+  ) => OpenAPIObject;
+
+  /**
+   * If `true`, the selector of OpenAPI definitions is displayed in the Swagger UI interface.
+   * Default: `false`.
+   */
   explorer?: boolean;
+
+  /**
+   * Additional Swagger UI options
+   */
   swaggerOptions?: SwaggerUiOptions;
+
+  /**
+   * Custom CSS styles to inject in Swagger UI page.
+   */
   customCss?: string;
+
+  /**
+   * URL(s) of a custom CSS stylesheet to load in Swagger UI page.
+   */
   customCssUrl?: string | string[];
+
+  /**
+   * URL(s) of custom JavaScript files to load in Swagger UI page.
+   */
   customJs?: string | string[];
+
+  /**
+   * Custom JavaScript scripts to load in Swagger UI page.
+   */
   customJsStr?: string | string[];
+
+  /**
+   * Custom favicon for Swagger UI page.
+   */
   customfavIcon?: string;
-  customSwaggerUiPath?: string;
-  swaggerUrl?: string;
+
+  /**
+   * Custom title for Swagger UI page.
+   */
   customSiteTitle?: string;
+
+  /**
+   * File system path (ex: ./node_modules/swagger-ui-dist) containing static Swagger UI assets.
+   */
+  customSwaggerUiPath?: string;
+
+  /**
+   * @deprecated This property has no effect.
+   */
   validatorUrl?: string;
+
+  /**
+   * @deprecated This property has no effect.
+   */
   url?: string;
+
+  /**
+   * @deprecated This property has no effect.
+   */
   urls?: Record<'url' | 'name', string>[];
-  jsonDocumentUrl?: string;
-  yamlDocumentUrl?: string;
-  patchDocumentOnRequest?: <TRequest = any, TResponse = any> (req: TRequest, res: TResponse, document: OpenAPIObject) => OpenAPIObject;
+
 }
diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts
index 521534d7..8f86df05 100644
--- a/lib/swagger-module.ts
+++ b/lib/swagger-module.ts
@@ -85,6 +85,7 @@ export class SwaggerModule {
     httpAdapter: HttpServer,
     documentOrFactory: OpenAPIObject | (() => OpenAPIObject),
     options: {
+      swaggerUiEnabled: boolean;
       jsonDocumentUrl: string;
       yamlDocumentUrl: string;
       swaggerOptions: SwaggerCustomOptions;
@@ -92,41 +93,64 @@ export class SwaggerModule {
   ) {
     let document: OpenAPIObject;
 
-    const lazyBuildDocument = () => {
-      return typeof documentOrFactory === 'function'
-        ? documentOrFactory()
-        : documentOrFactory;
+    const getBuiltDocument = () => {
+      if (!document) {
+        document =
+          typeof documentOrFactory === 'function'
+            ? documentOrFactory()
+            : documentOrFactory;
+      }
+      return document;
     };
 
+    if (options.swaggerUiEnabled) {
+      this.serveSwaggerUi(
+        finalPath,
+        urlLastSubdirectory,
+        httpAdapter,
+        getBuiltDocument,
+        options.swaggerOptions
+      );
+    }
+    this.serveDefinitions(httpAdapter, getBuiltDocument, options);
+  }
+
+  private static serveSwaggerUi(
+    finalPath: string,
+    urlLastSubdirectory: string,
+    httpAdapter: HttpServer,
+    getBuiltDocument: () => OpenAPIObject,
+    swaggerOptions: SwaggerCustomOptions
+  ) {
     const baseUrlForSwaggerUI = normalizeRelPath(`./${urlLastSubdirectory}/`);
 
-    let html: string;
-    let swaggerInitJS: string;
+    let swaggerUiHtml: string;
+    let swaggerUiInitJS: string;
 
     httpAdapter.get(
       normalizeRelPath(`${finalPath}/swagger-ui-init.js`),
       (req, res) => {
         res.type('application/javascript');
+        const document = getBuiltDocument();
 
-        if (!document) {
-          document = lazyBuildDocument();
-        }
-
-        if (options.swaggerOptions.patchDocumentOnRequest) {
-          const documentToSerialize =
-            options.swaggerOptions.patchDocumentOnRequest(req, res, document);
+        if (swaggerOptions.patchDocumentOnRequest) {
+          const documentToSerialize = swaggerOptions.patchDocumentOnRequest(
+            req,
+            res,
+            document
+          );
           const swaggerInitJsPerRequest = buildSwaggerInitJS(
             documentToSerialize,
-            options.swaggerOptions
+            swaggerOptions
           );
           return res.send(swaggerInitJsPerRequest);
         }
 
-        if (!swaggerInitJS) {
-          swaggerInitJS = buildSwaggerInitJS(document, options.swaggerOptions);
+        if (!swaggerUiInitJS) {
+          swaggerUiInitJS = buildSwaggerInitJS(document, swaggerOptions);
         }
 
-        res.send(swaggerInitJS);
+        res.send(swaggerUiInitJS);
       }
     );
 
@@ -141,29 +165,26 @@ export class SwaggerModule {
         ),
         (req, res) => {
           res.type('application/javascript');
+          const document = getBuiltDocument();
 
-          if (!document) {
-            document = lazyBuildDocument();
-          }
-
-          if (options.swaggerOptions.patchDocumentOnRequest) {
-            const documentToSerialize =
-              options.swaggerOptions.patchDocumentOnRequest(req, res, document);
+          if (swaggerOptions.patchDocumentOnRequest) {
+            const documentToSerialize = swaggerOptions.patchDocumentOnRequest(
+              req,
+              res,
+              document
+            );
             const swaggerInitJsPerRequest = buildSwaggerInitJS(
               documentToSerialize,
-              options.swaggerOptions
+              swaggerOptions
             );
             return res.send(swaggerInitJsPerRequest);
           }
 
-          if (!swaggerInitJS) {
-            swaggerInitJS = buildSwaggerInitJS(
-              document,
-              options.swaggerOptions
-            );
+          if (!swaggerUiInitJS) {
+            swaggerUiInitJS = buildSwaggerInitJS(document, swaggerOptions);
           }
 
-          res.send(swaggerInitJS);
+          res.send(swaggerUiInitJS);
         }
       );
     } catch (err) {
@@ -173,63 +194,26 @@ export class SwaggerModule {
        */
     }
 
-    httpAdapter.get(finalPath, (req, res) => {
+    httpAdapter.get(finalPath, (_, res) => {
       res.type('text/html');
 
-      if (!document) {
-        document = lazyBuildDocument();
+      if (!swaggerUiHtml) {
+        swaggerUiHtml = buildSwaggerHTML(baseUrlForSwaggerUI, swaggerOptions);
       }
 
-      if (options.swaggerOptions.patchDocumentOnRequest) {
-        const documentToSerialize =
-          options.swaggerOptions.patchDocumentOnRequest(req, res, document);
-        const htmlPerRequest = buildSwaggerHTML(
-          baseUrlForSwaggerUI,
-          documentToSerialize,
-          options.swaggerOptions
-        );
-        return res.send(htmlPerRequest);
-      }
-
-      if (!html) {
-        html = buildSwaggerHTML(
-          baseUrlForSwaggerUI,
-          document,
-          options.swaggerOptions
-        );
-      }
-
-      res.send(html);
+      res.send(swaggerUiHtml);
     });
 
     // fastify doesn't resolve 'routePath/' -> 'routePath', that's why we handle it manually
     try {
-      httpAdapter.get(normalizeRelPath(`${finalPath}/`), (req, res) => {
+      httpAdapter.get(normalizeRelPath(`${finalPath}/`), (_, res) => {
         res.type('text/html');
 
-        if (!document) {
-          document = lazyBuildDocument();
-        }
-
-        if (options.swaggerOptions.patchDocumentOnRequest) {
-          const documentToSerialize =
-            options.swaggerOptions.patchDocumentOnRequest(req, res, document);
-          const htmlPerRequest = buildSwaggerHTML(
-            baseUrlForSwaggerUI,
-            documentToSerialize,
-            options.swaggerOptions
-          );
-          return res.send(htmlPerRequest);
+        if (!swaggerUiHtml) {
+          swaggerUiHtml = buildSwaggerHTML(baseUrlForSwaggerUI, swaggerOptions);
         }
 
-        if (!html) {
-          html = buildSwaggerHTML(
-            baseUrlForSwaggerUI,
-            document,
-            options.swaggerOptions
-          );
-        }
-        res.send(html);
+        res.send(swaggerUiHtml);
       });
     } catch (err) {
       /**
@@ -239,13 +223,20 @@ export class SwaggerModule {
        * We can simply ignore that error here.
        */
     }
+  }
 
+  private static serveDefinitions(
+    httpAdapter: HttpServer,
+    getBuiltDocument: () => OpenAPIObject,
+    options: {
+      jsonDocumentUrl: string;
+      yamlDocumentUrl: string;
+      swaggerOptions: SwaggerCustomOptions;
+    }
+  ) {
     httpAdapter.get(normalizeRelPath(options.jsonDocumentUrl), (req, res) => {
       res.type('application/json');
-
-      if (!document) {
-        document = lazyBuildDocument();
-      }
+      const document = getBuiltDocument();
 
       const documentToSerialize = options.swaggerOptions.patchDocumentOnRequest
         ? options.swaggerOptions.patchDocumentOnRequest(req, res, document)
@@ -256,10 +247,7 @@ export class SwaggerModule {
 
     httpAdapter.get(normalizeRelPath(options.yamlDocumentUrl), (req, res) => {
       res.type('text/yaml');
-
-      if (!document) {
-        document = lazyBuildDocument();
-      }
+      const document = getBuiltDocument();
 
       const documentToSerialize = options.swaggerOptions.patchDocumentOnRequest
         ? options.swaggerOptions.patchDocumentOnRequest(req, res, document)
@@ -299,6 +287,8 @@ export class SwaggerModule {
       ? `${validatedGlobalPrefix}${validatePath(options.yamlDocumentUrl)}`
       : `${finalPath}-yaml`;
 
+    const swaggerUiEnabled = options?.swaggerUiEnabled ?? true;
+
     const httpAdapter = app.getHttpAdapter();
 
     SwaggerModule.serveDocuments(
@@ -307,24 +297,27 @@ export class SwaggerModule {
       httpAdapter,
       documentOrFactory,
       {
+        swaggerUiEnabled,
         jsonDocumentUrl: finalJSONDocumentPath,
         yamlDocumentUrl: finalYAMLDocumentPath,
         swaggerOptions: options || {}
       }
     );
 
-    SwaggerModule.serveStatic(finalPath, app, options?.customSwaggerUiPath);
-    /**
-     * Covers assets fetched through a relative path when Swagger url ends with a slash '/'.
-     * @see https://github.com/nestjs/swagger/issues/1976
-     */
-    const serveStaticSlashEndingPath = `${finalPath}/${urlLastSubdirectory}`;
-    /**
-     *  serveStaticSlashEndingPath === finalPath when path === '' || path === '/'
-     *  in that case we don't need to serve swagger assets on extra sub path
-     */
-    if (serveStaticSlashEndingPath !== finalPath) {
-      SwaggerModule.serveStatic(serveStaticSlashEndingPath, app);
+    if (swaggerUiEnabled) {
+      SwaggerModule.serveStatic(finalPath, app, options?.customSwaggerUiPath);
+      /**
+       * Covers assets fetched through a relative path when Swagger url ends with a slash '/'.
+       * @see https://github.com/nestjs/swagger/issues/1976
+       */
+      const serveStaticSlashEndingPath = `${finalPath}/${urlLastSubdirectory}`;
+      /**
+       *  serveStaticSlashEndingPath === finalPath when path === '' || path === '/'
+       *  in that case we don't need to serve swagger assets on extra sub path
+       */
+      if (serveStaticSlashEndingPath !== finalPath) {
+        SwaggerModule.serveStatic(serveStaticSlashEndingPath, app);
+      }
     }
   }
 }
diff --git a/lib/swagger-ui/swagger-ui.ts b/lib/swagger-ui/swagger-ui.ts
index 01f8645d..7df0cca4 100644
--- a/lib/swagger-ui/swagger-ui.ts
+++ b/lib/swagger-ui/swagger-ui.ts
@@ -66,7 +66,6 @@ function toTags(
  */
 export function buildSwaggerHTML(
   baseUrl: string,
-  swaggerDoc: OpenAPIObject,
   customOptions: SwaggerCustomOptions = {}
 ) {
   const {
diff --git a/package-lock.json b/package-lock.json
index 4e5ad7a0..86bb9047 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,7 +42,7 @@
         "openapi-types": "12.1.3",
         "prettier": "2.8.8",
         "reflect-metadata": "0.2.1",
-        "release-it": "17.0.5",
+        "release-it": "17.0.3",
         "supertest": "6.3.4",
         "swagger-parser": "10.0.3",
         "ts-jest": "29.1.2",
@@ -893,6 +893,39 @@
         "node": ">=v18"
       }
     },
+    "node_modules/@commitlint/is-ignored/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@commitlint/is-ignored/node_modules/semver": {
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@commitlint/is-ignored/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true
+    },
     "node_modules/@commitlint/lint": {
       "version": "18.6.1",
       "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-18.6.1.tgz",
@@ -2272,9 +2305,9 @@
       }
     },
     "node_modules/@sindresorhus/merge-streams": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.2.1.tgz",
-      "integrity": "sha512-255V7MMIKw6aQ43Wbqp9HZ+VHn6acddERTLiiLnlcPLU9PdTq9Aijl12oklAgUEblLWye+vHLzmqBx6f2TGcZw==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz",
+      "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==",
       "dev": true,
       "engines": {
         "node": ">=18"
@@ -5614,27 +5647,31 @@
       }
     },
     "node_modules/figures": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
-      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz",
+      "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==",
       "dev": true,
       "dependencies": {
-        "escape-string-regexp": "^1.0.5"
+        "escape-string-regexp": "^5.0.0",
+        "is-unicode-supported": "^1.2.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=14"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/figures/node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+      "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
       "dev": true,
       "engines": {
-        "node": ">=0.8.0"
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/file-entry-cache": {
@@ -6430,9 +6467,9 @@
       }
     },
     "node_modules/http-proxy-agent": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.1.tgz",
-      "integrity": "sha512-My1KCEPs6A0hb4qCVzYp8iEvA8j8YqcvXLZZH8C9OFuTYpYjHE7N2dtG3mRl1HMD4+VGXpF3XcDVcxGBT7yDZQ==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
+      "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
       "dev": true,
       "dependencies": {
         "agent-base": "^7.1.0",
@@ -6456,9 +6493,9 @@
       }
     },
     "node_modules/https-proxy-agent": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.3.tgz",
-      "integrity": "sha512-kCnwztfX0KZJSLOBrcL0emLeFako55NWMovvyPP2AjsghNk9RB1yjSI+jVumPHYZsNXegNoqupSW9IY3afSH8w==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
       "dev": true,
       "dependencies": {
         "agent-base": "^7.0.2",
@@ -6627,18 +6664,18 @@
       "dev": true
     },
     "node_modules/inquirer": {
-      "version": "9.2.14",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.14.tgz",
-      "integrity": "sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==",
+      "version": "9.2.12",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz",
+      "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==",
       "dev": true,
       "dependencies": {
-        "@ljharb/through": "^2.3.12",
+        "@ljharb/through": "^2.3.11",
         "ansi-escapes": "^4.3.2",
         "chalk": "^5.3.0",
         "cli-cursor": "^3.1.0",
         "cli-width": "^4.1.0",
         "external-editor": "^3.1.0",
-        "figures": "^3.2.0",
+        "figures": "^5.0.0",
         "lodash": "^4.17.21",
         "mute-stream": "1.0.0",
         "ora": "^5.4.1",
@@ -6649,7 +6686,7 @@
         "wrap-ansi": "^6.2.0"
       },
       "engines": {
-        "node": ">=18"
+        "node": ">=14.18.0"
       }
     },
     "node_modules/inquirer/node_modules/chalk": {
@@ -10272,15 +10309,15 @@
       }
     },
     "node_modules/proxy-agent": {
-      "version": "6.4.0",
-      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
-      "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz",
+      "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==",
       "dev": true,
       "dependencies": {
         "agent-base": "^7.0.2",
         "debug": "^4.3.4",
-        "http-proxy-agent": "^7.0.1",
-        "https-proxy-agent": "^7.0.3",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.2",
         "lru-cache": "^7.14.1",
         "pac-proxy-agent": "^7.0.1",
         "proxy-from-env": "^1.1.0",
@@ -10686,9 +10723,9 @@
       }
     },
     "node_modules/release-it": {
-      "version": "17.0.5",
-      "resolved": "https://registry.npmjs.org/release-it/-/release-it-17.0.5.tgz",
-      "integrity": "sha512-97FcBu/2IjPd4qMvliZSnavpCO0+BWi8lZr+BgVJTXs6ihX8HLPucYR5RsgUUcZoGKj3beO8cunrsMuvEWTzGQ==",
+      "version": "17.0.3",
+      "resolved": "https://registry.npmjs.org/release-it/-/release-it-17.0.3.tgz",
+      "integrity": "sha512-QjTCmvQm91pwLEbvavEs9jofHNe8thsb9Uimin+8DNSwFRdUd73p0Owy2PP/Dzh/EegRkKq/o+4Pn1xp8pC1og==",
       "dev": true,
       "funding": [
         {
@@ -10708,9 +10745,9 @@
         "cosmiconfig": "9.0.0",
         "execa": "8.0.1",
         "git-url-parse": "14.0.0",
-        "globby": "14.0.1",
+        "globby": "14.0.0",
         "got": "13.0.0",
-        "inquirer": "9.2.14",
+        "inquirer": "9.2.12",
         "is-ci": "3.0.1",
         "issue-parser": "6.0.0",
         "lodash": "4.17.21",
@@ -10721,8 +10758,8 @@
         "ora": "8.0.1",
         "os-name": "5.1.0",
         "promise.allsettled": "1.0.7",
-        "proxy-agent": "6.4.0",
-        "semver": "7.6.0",
+        "proxy-agent": "6.3.1",
+        "semver": "7.5.4",
         "shelljs": "0.8.5",
         "update-notifier": "7.0.0",
         "url-join": "5.0.0",
@@ -10819,12 +10856,12 @@
       }
     },
     "node_modules/release-it/node_modules/globby": {
-      "version": "14.0.1",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz",
-      "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==",
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz",
+      "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==",
       "dev": true,
       "dependencies": {
-        "@sindresorhus/merge-streams": "^2.1.0",
+        "@sindresorhus/merge-streams": "^1.0.0",
         "fast-glob": "^3.3.2",
         "ignore": "^5.2.4",
         "path-type": "^5.0.0",
@@ -11328,9 +11365,9 @@
       "dev": true
     },
     "node_modules/semver": {
-      "version": "7.6.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
-      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
diff --git a/package.json b/package.json
index 781fce4f..bba65a29 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
     "openapi-types": "12.1.3",
     "prettier": "2.8.8",
     "reflect-metadata": "0.2.1",
-    "release-it": "17.0.5",
+    "release-it": "17.0.3",
     "supertest": "6.3.4",
     "swagger-parser": "10.0.3",
     "ts-jest": "29.1.2",


Test patch:
diff --git a/e2e/express.e2e-spec.ts b/e2e/express.e2e-spec.ts
index 5a3f1167..25610c17 100644
--- a/e2e/express.e2e-spec.ts
+++ b/e2e/express.e2e-spec.ts
@@ -86,7 +86,7 @@ describe('Express Swagger', () => {
       );
       SwaggerModule.setup(SWAGGER_RELATIVE_URL, app, swaggerDocument, {
         // to showcase that in new implementation u can use custom swagger-ui path. Useful when using e.g. webpack
-        customSwaggerUiPath: path.resolve(`./node_modules/swagger-ui-dist`),
+        customSwaggerUiPath: path.resolve(`./node_modules/swagger-ui-dist`)
       });
 
       await app.init();
@@ -114,6 +114,55 @@ describe('Express Swagger', () => {
     });
   });
 
+  describe('disabled Swagger UI but served JSON/YAML definitions', () => {
+    const SWAGGER_RELATIVE_URL = '/apidoc';
+
+    beforeEach(async () => {
+      const swaggerDocument = SwaggerModule.createDocument(
+        app,
+        builder.build()
+      );
+      SwaggerModule.setup(SWAGGER_RELATIVE_URL, app, swaggerDocument, {
+        swaggerUiEnabled: false
+      });
+
+      await app.init();
+    });
+
+    afterEach(async () => {
+      await app.close();
+    });
+
+    it('should serve the JSON definition file', async () => {
+      const response = await request(app.getHttpServer()).get(
+        `${SWAGGER_RELATIVE_URL}-json`
+      );
+
+      expect(response.status).toEqual(200);
+      expect(Object.keys(response.body).length).toBeGreaterThan(0);
+    });
+
+    it('should serve the YAML definition file', async () => {
+      const response = await request(app.getHttpServer()).get(
+        `${SWAGGER_RELATIVE_URL}-yaml`
+      );
+
+      expect(response.status).toEqual(200);
+      expect(response.text.length).toBeGreaterThan(0);
+    });
+
+    it.each([
+      '/apidoc',
+      '/apidoc/',
+      '/apidoc/swagger-ui-bundle.js',
+      '/apidoc/swagger-ui-init.js'
+    ])('should not serve "%s"', async (file) => {
+      const response = await request(app.getHttpServer()).get(file);
+
+      expect(response.status).toEqual(404);
+    });
+  });
+
   describe('custom documents endpoints', () => {
     const JSON_CUSTOM_URL = '/apidoc-json';
     const YAML_CUSTOM_URL = '/apidoc-yaml';
@@ -154,10 +203,10 @@ describe('Express Swagger', () => {
         `${JSON_CUSTOM_URL}?description=My%20custom%20description`
       );
 
-      expect(response.body.info.description).toBe("My custom description");
+      expect(response.body.info.description).toBe('My custom description');
     });
 
-    it('yaml document should be server in the custom url', async () => {
+    it('yaml document should be served in the custom url', async () => {
       const response = await request(app.getHttpServer()).get(YAML_CUSTOM_URL);
 
       expect(response.status).toEqual(200);
@@ -168,7 +217,7 @@ describe('Express Swagger', () => {
       const response = await request(app.getHttpServer()).get(
         `${YAML_CUSTOM_URL}?description=My%20custom%20description`
       );
-      expect(response.text).toContain("My custom description");
+      expect(response.text).toContain('My custom description');
     });
   });
 
@@ -244,13 +293,17 @@ describe('Express Swagger', () => {
         customfavIcon: CUSTOM_FAVICON,
         customSiteTitle: CUSTOM_SITE_TITLE,
         customCssUrl: CUSTOM_CSS_URL,
-        patchDocumentOnRequest<ExpressRequest, ExpressResponse> (req, res, document) {
+        patchDocumentOnRequest<ExpressRequest, ExpressResponse>(
+          req,
+          res,
+          document
+        ) {
           return {
             ...document,
             info: {
               description: req.query.description
             }
-          }
+          };
         }
       });
 
@@ -313,23 +366,29 @@ describe('Express Swagger', () => {
       );
 
       SwaggerModule.setup('/:customer/', app, swaggerDocument, {
-        patchDocumentOnRequest<ExpressRequest, ExpressResponse> (req, res, document) {
+        patchDocumentOnRequest<ExpressRequest, ExpressResponse>(
+          req,
+          res,
+          document
+        ) {
           return {
             ...document,
             info: {
               description: `${req.params.customer}'s API documentation`
             }
-          }
+          };
         }
       });
 
       await app.init();
 
-      const response: Response = await request(app.getHttpServer()).get('/customer-1/swagger-ui-init.js');
+      const response: Response = await request(app.getHttpServer()).get(
+        '/customer-1/swagger-ui-init.js'
+      );
 
       await app.close();
       expect(response.text).toContain("customer-1's API documentation");
-    })
+    });
 
     afterEach(async () => {
       await app.close();