diff --git a/e2e/express.e2e-spec.ts b/e2e/express.e2e-spec.ts
index 5a3f11677..25610c17a 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();
diff --git a/lib/interfaces/swagger-custom-options.interface.ts b/lib/interfaces/swagger-custom-options.interface.ts
index a28e6d3aa..aa3c15c49 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 521534d7f..8f86df056 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 01f8645df..7df0cca4f 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 {