Skip to content

Commit c541886

Browse files
authoredJul 1, 2024··
Merge pull request #2840 from lucas-gregoire/feat/allow-to-disable-swagger-ui
feat: allow to disable Swagger UI
2 parents 91a7c76 + d56087c commit c541886

File tree

4 files changed

+248
-113
lines changed

4 files changed

+248
-113
lines changed
 

‎e2e/express.e2e-spec.ts

+69-10
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('Express Swagger', () => {
8686
);
8787
SwaggerModule.setup(SWAGGER_RELATIVE_URL, app, swaggerDocument, {
8888
// to showcase that in new implementation u can use custom swagger-ui path. Useful when using e.g. webpack
89-
customSwaggerUiPath: path.resolve(`./node_modules/swagger-ui-dist`),
89+
customSwaggerUiPath: path.resolve(`./node_modules/swagger-ui-dist`)
9090
});
9191

9292
await app.init();
@@ -114,6 +114,55 @@ describe('Express Swagger', () => {
114114
});
115115
});
116116

117+
describe('disabled Swagger UI but served JSON/YAML definitions', () => {
118+
const SWAGGER_RELATIVE_URL = '/apidoc';
119+
120+
beforeEach(async () => {
121+
const swaggerDocument = SwaggerModule.createDocument(
122+
app,
123+
builder.build()
124+
);
125+
SwaggerModule.setup(SWAGGER_RELATIVE_URL, app, swaggerDocument, {
126+
swaggerUiEnabled: false
127+
});
128+
129+
await app.init();
130+
});
131+
132+
afterEach(async () => {
133+
await app.close();
134+
});
135+
136+
it('should serve the JSON definition file', async () => {
137+
const response = await request(app.getHttpServer()).get(
138+
`${SWAGGER_RELATIVE_URL}-json`
139+
);
140+
141+
expect(response.status).toEqual(200);
142+
expect(Object.keys(response.body).length).toBeGreaterThan(0);
143+
});
144+
145+
it('should serve the YAML definition file', async () => {
146+
const response = await request(app.getHttpServer()).get(
147+
`${SWAGGER_RELATIVE_URL}-yaml`
148+
);
149+
150+
expect(response.status).toEqual(200);
151+
expect(response.text.length).toBeGreaterThan(0);
152+
});
153+
154+
it.each([
155+
'/apidoc',
156+
'/apidoc/',
157+
'/apidoc/swagger-ui-bundle.js',
158+
'/apidoc/swagger-ui-init.js'
159+
])('should not serve "%s"', async (file) => {
160+
const response = await request(app.getHttpServer()).get(file);
161+
162+
expect(response.status).toEqual(404);
163+
});
164+
});
165+
117166
describe('custom documents endpoints', () => {
118167
const JSON_CUSTOM_URL = '/apidoc-json';
119168
const YAML_CUSTOM_URL = '/apidoc-yaml';
@@ -154,10 +203,10 @@ describe('Express Swagger', () => {
154203
`${JSON_CUSTOM_URL}?description=My%20custom%20description`
155204
);
156205

157-
expect(response.body.info.description).toBe("My custom description");
206+
expect(response.body.info.description).toBe('My custom description');
158207
});
159208

160-
it('yaml document should be server in the custom url', async () => {
209+
it('yaml document should be served in the custom url', async () => {
161210
const response = await request(app.getHttpServer()).get(YAML_CUSTOM_URL);
162211

163212
expect(response.status).toEqual(200);
@@ -168,7 +217,7 @@ describe('Express Swagger', () => {
168217
const response = await request(app.getHttpServer()).get(
169218
`${YAML_CUSTOM_URL}?description=My%20custom%20description`
170219
);
171-
expect(response.text).toContain("My custom description");
220+
expect(response.text).toContain('My custom description');
172221
});
173222
});
174223

@@ -244,13 +293,17 @@ describe('Express Swagger', () => {
244293
customfavIcon: CUSTOM_FAVICON,
245294
customSiteTitle: CUSTOM_SITE_TITLE,
246295
customCssUrl: CUSTOM_CSS_URL,
247-
patchDocumentOnRequest<ExpressRequest, ExpressResponse> (req, res, document) {
296+
patchDocumentOnRequest<ExpressRequest, ExpressResponse>(
297+
req,
298+
res,
299+
document
300+
) {
248301
return {
249302
...document,
250303
info: {
251304
description: req.query.description
252305
}
253-
}
306+
};
254307
}
255308
});
256309

@@ -313,23 +366,29 @@ describe('Express Swagger', () => {
313366
);
314367

315368
SwaggerModule.setup('/:customer/', app, swaggerDocument, {
316-
patchDocumentOnRequest<ExpressRequest, ExpressResponse> (req, res, document) {
369+
patchDocumentOnRequest<ExpressRequest, ExpressResponse>(
370+
req,
371+
res,
372+
document
373+
) {
317374
return {
318375
...document,
319376
info: {
320377
description: `${req.params.customer}'s API documentation`
321378
}
322-
}
379+
};
323380
}
324381
});
325382

326383
await app.init();
327384

328-
const response: Response = await request(app.getHttpServer()).get('/customer-1/swagger-ui-init.js');
385+
const response: Response = await request(app.getHttpServer()).get(
386+
'/customer-1/swagger-ui-init.js'
387+
);
329388

330389
await app.close();
331390
expect(response.text).toContain("customer-1's API documentation");
332-
})
391+
});
333392

334393
afterEach(async () => {
335394
await app.close();
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,107 @@
11
import { SwaggerUiOptions } from './swagger-ui-options.interface';
2-
import { SwaggerDocumentOptions } from './swagger-document-options.interface';
32
import { OpenAPIObject } from './open-api-spec.interface';
43

54
export interface SwaggerCustomOptions {
5+
/**
6+
* If `true`, Swagger resources paths will be prefixed by the global prefix set through `setGlobalPrefix()`.
7+
* Default: `false`.
8+
* @see https://docs.nestjs.com/faq/global-prefix
9+
*/
610
useGlobalPrefix?: boolean;
11+
12+
/**
13+
* If `false`, only API definitions (JSON and YAML) will be served (on `/{path}-json` and `/{path}-yaml`).
14+
* This is particularly useful if you are already hosting a Swagger UI somewhere else and just want to serve API definitions.
15+
* Default: `true`.
16+
*/
17+
swaggerUiEnabled?: boolean;
18+
19+
/**
20+
* Url point the API definition to load in Swagger UI.
21+
*/
22+
swaggerUrl?: string;
23+
24+
/**
25+
* Path of the JSON API definition to serve.
26+
* Default: `{{path}}-json`.
27+
*/
28+
jsonDocumentUrl?: string;
29+
30+
/**
31+
* Path of the YAML API definition to serve.
32+
* Default: `{{path}}-json`.
33+
*/
34+
yamlDocumentUrl?: string;
35+
36+
/**
37+
* Hook allowing to alter the OpenAPI document before being served.
38+
* It's called after the document is generated and before it is served as JSON & YAML.
39+
*/
40+
patchDocumentOnRequest?: <TRequest = any, TResponse = any>(
41+
req: TRequest,
42+
res: TResponse,
43+
document: OpenAPIObject
44+
) => OpenAPIObject;
45+
46+
/**
47+
* If `true`, the selector of OpenAPI definitions is displayed in the Swagger UI interface.
48+
* Default: `false`.
49+
*/
750
explorer?: boolean;
51+
52+
/**
53+
* Additional Swagger UI options
54+
*/
855
swaggerOptions?: SwaggerUiOptions;
56+
57+
/**
58+
* Custom CSS styles to inject in Swagger UI page.
59+
*/
960
customCss?: string;
61+
62+
/**
63+
* URL(s) of a custom CSS stylesheet to load in Swagger UI page.
64+
*/
1065
customCssUrl?: string | string[];
66+
67+
/**
68+
* URL(s) of custom JavaScript files to load in Swagger UI page.
69+
*/
1170
customJs?: string | string[];
71+
72+
/**
73+
* Custom JavaScript scripts to load in Swagger UI page.
74+
*/
1275
customJsStr?: string | string[];
76+
77+
/**
78+
* Custom favicon for Swagger UI page.
79+
*/
1380
customfavIcon?: string;
14-
customSwaggerUiPath?: string;
15-
swaggerUrl?: string;
81+
82+
/**
83+
* Custom title for Swagger UI page.
84+
*/
1685
customSiteTitle?: string;
86+
87+
/**
88+
* File system path (ex: ./node_modules/swagger-ui-dist) containing static Swagger UI assets.
89+
*/
90+
customSwaggerUiPath?: string;
91+
92+
/**
93+
* @deprecated This property has no effect.
94+
*/
1795
validatorUrl?: string;
96+
97+
/**
98+
* @deprecated This property has no effect.
99+
*/
18100
url?: string;
101+
102+
/**
103+
* @deprecated This property has no effect.
104+
*/
19105
urls?: Record<'url' | 'name', string>[];
20-
jsonDocumentUrl?: string;
21-
yamlDocumentUrl?: string;
22-
patchDocumentOnRequest?: <TRequest = any, TResponse = any> (req: TRequest, res: TResponse, document: OpenAPIObject) => OpenAPIObject;
106+
23107
}

‎lib/swagger-module.ts

+89-96
Original file line numberDiff line numberDiff line change
@@ -85,48 +85,72 @@ export class SwaggerModule {
8585
httpAdapter: HttpServer,
8686
documentOrFactory: OpenAPIObject | (() => OpenAPIObject),
8787
options: {
88+
swaggerUiEnabled: boolean;
8889
jsonDocumentUrl: string;
8990
yamlDocumentUrl: string;
9091
swaggerOptions: SwaggerCustomOptions;
9192
}
9293
) {
9394
let document: OpenAPIObject;
9495

95-
const lazyBuildDocument = () => {
96-
return typeof documentOrFactory === 'function'
97-
? documentOrFactory()
98-
: documentOrFactory;
96+
const getBuiltDocument = () => {
97+
if (!document) {
98+
document =
99+
typeof documentOrFactory === 'function'
100+
? documentOrFactory()
101+
: documentOrFactory;
102+
}
103+
return document;
99104
};
100105

106+
if (options.swaggerUiEnabled) {
107+
this.serveSwaggerUi(
108+
finalPath,
109+
urlLastSubdirectory,
110+
httpAdapter,
111+
getBuiltDocument,
112+
options.swaggerOptions
113+
);
114+
}
115+
this.serveDefinitions(httpAdapter, getBuiltDocument, options);
116+
}
117+
118+
private static serveSwaggerUi(
119+
finalPath: string,
120+
urlLastSubdirectory: string,
121+
httpAdapter: HttpServer,
122+
getBuiltDocument: () => OpenAPIObject,
123+
swaggerOptions: SwaggerCustomOptions
124+
) {
101125
const baseUrlForSwaggerUI = normalizeRelPath(`./${urlLastSubdirectory}/`);
102126

103-
let html: string;
104-
let swaggerInitJS: string;
127+
let swaggerUiHtml: string;
128+
let swaggerUiInitJS: string;
105129

106130
httpAdapter.get(
107131
normalizeRelPath(`${finalPath}/swagger-ui-init.js`),
108132
(req, res) => {
109133
res.type('application/javascript');
134+
const document = getBuiltDocument();
110135

111-
if (!document) {
112-
document = lazyBuildDocument();
113-
}
114-
115-
if (options.swaggerOptions.patchDocumentOnRequest) {
116-
const documentToSerialize =
117-
options.swaggerOptions.patchDocumentOnRequest(req, res, document);
136+
if (swaggerOptions.patchDocumentOnRequest) {
137+
const documentToSerialize = swaggerOptions.patchDocumentOnRequest(
138+
req,
139+
res,
140+
document
141+
);
118142
const swaggerInitJsPerRequest = buildSwaggerInitJS(
119143
documentToSerialize,
120-
options.swaggerOptions
144+
swaggerOptions
121145
);
122146
return res.send(swaggerInitJsPerRequest);
123147
}
124148

125-
if (!swaggerInitJS) {
126-
swaggerInitJS = buildSwaggerInitJS(document, options.swaggerOptions);
149+
if (!swaggerUiInitJS) {
150+
swaggerUiInitJS = buildSwaggerInitJS(document, swaggerOptions);
127151
}
128152

129-
res.send(swaggerInitJS);
153+
res.send(swaggerUiInitJS);
130154
}
131155
);
132156

@@ -141,29 +165,26 @@ export class SwaggerModule {
141165
),
142166
(req, res) => {
143167
res.type('application/javascript');
168+
const document = getBuiltDocument();
144169

145-
if (!document) {
146-
document = lazyBuildDocument();
147-
}
148-
149-
if (options.swaggerOptions.patchDocumentOnRequest) {
150-
const documentToSerialize =
151-
options.swaggerOptions.patchDocumentOnRequest(req, res, document);
170+
if (swaggerOptions.patchDocumentOnRequest) {
171+
const documentToSerialize = swaggerOptions.patchDocumentOnRequest(
172+
req,
173+
res,
174+
document
175+
);
152176
const swaggerInitJsPerRequest = buildSwaggerInitJS(
153177
documentToSerialize,
154-
options.swaggerOptions
178+
swaggerOptions
155179
);
156180
return res.send(swaggerInitJsPerRequest);
157181
}
158182

159-
if (!swaggerInitJS) {
160-
swaggerInitJS = buildSwaggerInitJS(
161-
document,
162-
options.swaggerOptions
163-
);
183+
if (!swaggerUiInitJS) {
184+
swaggerUiInitJS = buildSwaggerInitJS(document, swaggerOptions);
164185
}
165186

166-
res.send(swaggerInitJS);
187+
res.send(swaggerUiInitJS);
167188
}
168189
);
169190
} catch (err) {
@@ -173,63 +194,26 @@ export class SwaggerModule {
173194
*/
174195
}
175196

176-
httpAdapter.get(finalPath, (req, res) => {
197+
httpAdapter.get(finalPath, (_, res) => {
177198
res.type('text/html');
178199

179-
if (!document) {
180-
document = lazyBuildDocument();
200+
if (!swaggerUiHtml) {
201+
swaggerUiHtml = buildSwaggerHTML(baseUrlForSwaggerUI, swaggerOptions);
181202
}
182203

183-
if (options.swaggerOptions.patchDocumentOnRequest) {
184-
const documentToSerialize =
185-
options.swaggerOptions.patchDocumentOnRequest(req, res, document);
186-
const htmlPerRequest = buildSwaggerHTML(
187-
baseUrlForSwaggerUI,
188-
documentToSerialize,
189-
options.swaggerOptions
190-
);
191-
return res.send(htmlPerRequest);
192-
}
193-
194-
if (!html) {
195-
html = buildSwaggerHTML(
196-
baseUrlForSwaggerUI,
197-
document,
198-
options.swaggerOptions
199-
);
200-
}
201-
202-
res.send(html);
204+
res.send(swaggerUiHtml);
203205
});
204206

205207
// fastify doesn't resolve 'routePath/' -> 'routePath', that's why we handle it manually
206208
try {
207-
httpAdapter.get(normalizeRelPath(`${finalPath}/`), (req, res) => {
209+
httpAdapter.get(normalizeRelPath(`${finalPath}/`), (_, res) => {
208210
res.type('text/html');
209211

210-
if (!document) {
211-
document = lazyBuildDocument();
212-
}
213-
214-
if (options.swaggerOptions.patchDocumentOnRequest) {
215-
const documentToSerialize =
216-
options.swaggerOptions.patchDocumentOnRequest(req, res, document);
217-
const htmlPerRequest = buildSwaggerHTML(
218-
baseUrlForSwaggerUI,
219-
documentToSerialize,
220-
options.swaggerOptions
221-
);
222-
return res.send(htmlPerRequest);
212+
if (!swaggerUiHtml) {
213+
swaggerUiHtml = buildSwaggerHTML(baseUrlForSwaggerUI, swaggerOptions);
223214
}
224215

225-
if (!html) {
226-
html = buildSwaggerHTML(
227-
baseUrlForSwaggerUI,
228-
document,
229-
options.swaggerOptions
230-
);
231-
}
232-
res.send(html);
216+
res.send(swaggerUiHtml);
233217
});
234218
} catch (err) {
235219
/**
@@ -239,13 +223,20 @@ export class SwaggerModule {
239223
* We can simply ignore that error here.
240224
*/
241225
}
226+
}
242227

228+
private static serveDefinitions(
229+
httpAdapter: HttpServer,
230+
getBuiltDocument: () => OpenAPIObject,
231+
options: {
232+
jsonDocumentUrl: string;
233+
yamlDocumentUrl: string;
234+
swaggerOptions: SwaggerCustomOptions;
235+
}
236+
) {
243237
httpAdapter.get(normalizeRelPath(options.jsonDocumentUrl), (req, res) => {
244238
res.type('application/json');
245-
246-
if (!document) {
247-
document = lazyBuildDocument();
248-
}
239+
const document = getBuiltDocument();
249240

250241
const documentToSerialize = options.swaggerOptions.patchDocumentOnRequest
251242
? options.swaggerOptions.patchDocumentOnRequest(req, res, document)
@@ -256,10 +247,7 @@ export class SwaggerModule {
256247

257248
httpAdapter.get(normalizeRelPath(options.yamlDocumentUrl), (req, res) => {
258249
res.type('text/yaml');
259-
260-
if (!document) {
261-
document = lazyBuildDocument();
262-
}
250+
const document = getBuiltDocument();
263251

264252
const documentToSerialize = options.swaggerOptions.patchDocumentOnRequest
265253
? options.swaggerOptions.patchDocumentOnRequest(req, res, document)
@@ -299,6 +287,8 @@ export class SwaggerModule {
299287
? `${validatedGlobalPrefix}${validatePath(options.yamlDocumentUrl)}`
300288
: `${finalPath}-yaml`;
301289

290+
const swaggerUiEnabled = options?.swaggerUiEnabled ?? true;
291+
302292
const httpAdapter = app.getHttpAdapter();
303293

304294
SwaggerModule.serveDocuments(
@@ -307,24 +297,27 @@ export class SwaggerModule {
307297
httpAdapter,
308298
documentOrFactory,
309299
{
300+
swaggerUiEnabled,
310301
jsonDocumentUrl: finalJSONDocumentPath,
311302
yamlDocumentUrl: finalYAMLDocumentPath,
312303
swaggerOptions: options || {}
313304
}
314305
);
315306

316-
SwaggerModule.serveStatic(finalPath, app, options?.customSwaggerUiPath);
317-
/**
318-
* Covers assets fetched through a relative path when Swagger url ends with a slash '/'.
319-
* @see https://github.com/nestjs/swagger/issues/1976
320-
*/
321-
const serveStaticSlashEndingPath = `${finalPath}/${urlLastSubdirectory}`;
322-
/**
323-
* serveStaticSlashEndingPath === finalPath when path === '' || path === '/'
324-
* in that case we don't need to serve swagger assets on extra sub path
325-
*/
326-
if (serveStaticSlashEndingPath !== finalPath) {
327-
SwaggerModule.serveStatic(serveStaticSlashEndingPath, app);
307+
if (swaggerUiEnabled) {
308+
SwaggerModule.serveStatic(finalPath, app, options?.customSwaggerUiPath);
309+
/**
310+
* Covers assets fetched through a relative path when Swagger url ends with a slash '/'.
311+
* @see https://github.com/nestjs/swagger/issues/1976
312+
*/
313+
const serveStaticSlashEndingPath = `${finalPath}/${urlLastSubdirectory}`;
314+
/**
315+
* serveStaticSlashEndingPath === finalPath when path === '' || path === '/'
316+
* in that case we don't need to serve swagger assets on extra sub path
317+
*/
318+
if (serveStaticSlashEndingPath !== finalPath) {
319+
SwaggerModule.serveStatic(serveStaticSlashEndingPath, app);
320+
}
328321
}
329322
}
330323
}

‎lib/swagger-ui/swagger-ui.ts

-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ function toTags(
6666
*/
6767
export function buildSwaggerHTML(
6868
baseUrl: string,
69-
swaggerDoc: OpenAPIObject,
7069
customOptions: SwaggerCustomOptions = {}
7170
) {
7271
const {

0 commit comments

Comments
 (0)
Please sign in to comment.