OpenAPI & Swagger UI
Introduction
OpenAPI Specification (formerly known as Swagger Specification) is an API description format for REST APIs. An OpenAPI document allows developers to describe entirely an API.
Swagger UI is a graphical interface to visualize and interact with the API’s resources. It is automatically generated from one or several OpenAPI documents.
Example of OpenAPI document and Swagger Visualisation
Quick Start
This example shows how to generate a documentation page of your API directly from your hooks and controllers.
app.controller.ts
import { controller } from '@foal/core';
import { ApiController, OpenApiController } from './controllers';
export class AppController {
subControllers = [
controller('/api', ApiController),
controller('/swagger', OpenApiController),
]
}
api.controller.ts
import { ApiInfo, ApiServer, Context, Post, ValidateBody } from '@foal/core';
import { JWTRequired } from '@foal/jwt';
@ApiInfo({
title: 'A Great API',
version: '1.0.0'
})
@ApiServer({
url: '/api'
})
@JWTRequired()
export class ApiController {
@Post('/products')
@ValidateBody({
type: 'object',
properties: {
name: { type: 'string' }
},
required: [ 'name' ],
additionalProperties: false,
})
createProduct(ctx: Context) {
// ...
}
}
openapi.controller.ts
import { SwaggerController } from '@foal/swagger';
import { ApiController } from './api.controller';
export class OpenApiController extends SwaggerController {
options = { controllerClass: ApiController };
}
Result
OpenAPI
The Basics
The first thing to do is to add the @ApiInfo
decorator to the root controller of the API. Two attributes are required: the title
and the version
of the API.
import { ApiInfo } from '@foal/core';
@ApiInfo({
title: 'A Great API',
version: '1.0.0'
})
// @ApiServer({
// url: '/api'
// })
export class ApiController {
// ...
}
Then each controller method can be documented with the @ApiOperation
decorator.
import { ApiOperation, Get } from '@foal/core';
// ...
export class ApiController {
@Get('/products')
@ApiOperation({
responses: {
200: {
content: {
'application/json': {
schema: {
items: {
properties: {
name: { type: 'string' }
},
type: 'object',
required: [ 'name' ]
},
type: 'array',
}
}
},
description: 'successful operation'
}
},
summary: 'Return a list of all the products.'
})
readProducts() {
// ...
}
}
Beside the @ApiOperation
decorator, you can also use other decorators more specific to improve the readability of the code.
Operation Decorators |
---|
@ApiOperationSummary |
@ApiOperationId |
@ApiOperationDescription |
@ApiServer |
@ApiRequestBody |
@ApiSecurityRequirement |
@ApiDefineTag |
@ApiExternalDoc |
@ApiUseTag |
@ApiParameter |
@ApiResponse |
@ApiCallback |
Example
import { ApiOperation, ApiResponse, Get } from '@foal/core';
// ...
export class ApiController {
@Get('/products')
@ApiOperation({
responses: {
200: {
description: 'successful operation'
},
404: {
description: 'not found'
},
}
})
readProducts() {
// ...
}
// is equivalent to
@Get('/products')
@ApiResponse(200, { description: 'successful operation' })
@ApiResponse(404, { description: 'not found' })
readProducts() {
// ...
}
}
Don't Repeat Yourself and Decorate Sub-Controllers
Large applications can have many subcontrollers. FoalTS automatically resolves the paths for you and allows you to share common specifications between several operations.
Example
import { ApiDeprecated, ApiInfo, ApiResponse, controller, Get } from '@foal/core';
@ApiInfo({
title: 'A Great API',
version: '1.0.0'
})
export class ApiController {
subControllers = [
controller('/products', ProductController)
];
}
// All the operations of this controller and
// its subcontrollers should be deprecated.
@ApiDeprecated()
class ProductController {
@Get()
@ApiResponse(200, { description: 'successful operation' })
readProducts() {
// ...
}
@Get('/:productId')
@ApiResponse(200, { description: 'successful operation' })
@ApiResponse(404, { description: 'not found' })
readProduct() {
// ...
}
}
The generated document will then look like this:
openapi: 3.0.0
info:
title: 'A Great API'
version: 1.0.0
paths:
/products: # The path is computed automatically
get:
deprecated: true # The operation is deprecated
responses:
200:
description: successful operation
/products/{productId}: # The path is computed automatically
get:
deprecated: true # The operation is deprecated
responses:
200:
description: successful operation
404:
description: not found
Use Existing Hooks
The addition of these decorators can be quite redundant with existing hooks. For example, if we want to write OpenAPI documentation for authentication and validation of the request body, we may end up with something like this.
@JWTRequired()
@ApiSecurityRequirement({ bearerAuth: [] })
@ApiDefineSecurityScheme('bearerAuth', {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
})
export class ApiController {
@Post('/products')
@ValidateBody(schema)
@ApiRequestBody({
required: true,
content: {
'application/json': { schema }
}
})
createProducts() {
}
}
To avoid this, the framework hooks already expose an API specification which is directly included in the generated OpenAPI document.
@JWTRequired()
export class ApiController {
@Post('/products')
@ValidateBody(schema)
createProducts() {
// ...
}
}
You can disable this behavior globally with the configuration key setting.openapi.useHooks
.
- YAML
- JSON
- JS
settings:
openapi:
useHooks: false
{
"settings": {
"openapi": {
"useHooks": false
}
}
}
module.exports = {
settings: {
openapi: {
useHooks: false
}
}
}
You can also disable it on a specific hook with the openapi
option.
export class ApiController {
@Post('/products')
// Generate automatically the OpenAPI spec for the request body
@ValidateBody(schema)
// Choose to write a customize spec for the path parameters
@ValidateParams(schema2, { openapi: false })
@ApiParameter( ... )
createProducts() {
// ...
}
}
Generate the API Document
Once the controllers are decorated, there are several ways to generate the OpenAPI document.
from the controllers
Documents can be retrieved with the OpenApi
service:
import { dependency, OpenApi } from '@foal/core';
class Service {
@dependency
openApi: OpenApi;
foo() {
const document = this.openApi.getDocument(ApiController);
}
}