Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow to disable Swagger UI #2840

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 69 additions & 10 deletions e2e/express.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
Comment on lines +154 to +163
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All Swagger UI resources are throwing 404 when disabled

});

describe('custom documents endpoints', () => {
const JSON_CUSTOM_URL = '/apidoc-json';
const YAML_CUSTOM_URL = '/apidoc-yaml';
Expand Down Expand Up @@ -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);
Expand All @@ -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');
});
});

Expand Down Expand Up @@ -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
}
}
};
}
});

Expand Down Expand Up @@ -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();
Expand Down
96 changes: 90 additions & 6 deletions lib/interfaces/swagger-custom-options.interface.ts
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +12 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for aligning the code with what's actually documented at https://docs.nestjs.com/openapi/introduction#setup-options

Copy link

@motabass motabass May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it in the docs but not in the code? Made me scratch my head for half an hour..

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ulidtko @kamilmysliwiec

agreeing to @motabass, the docs are live already, can this be merged please?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, would be nice if this would be merged, it's quite confusing that it's in the docs but you can't actually use it 😬

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree merge it, the docs are live already, i want use selfdefine openapi-ui

Copy link
Contributor Author

@lucas-gregoire lucas-gregoire Jun 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry that both PRs haven't been merged simultaneously (or at least the code before the docs). The PR for the docs was merged in 1 day, while this one has been waiting (and ready) for 4 months 😿

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For those who want to try this feature before its release, I've published a forked package @lsge/swagger that you can install as a replacement for @nestjs/swagger:

Change this in your package.json:

-    "@nestjs/swagger": "7.4.0",
+    "@nestjs/swagger": "npm:@lsge/[email protected]",


/**
* 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;

}
Loading