@martin.xyz/openapi-decorators is a framework agnostic library to automatically generate OpenAPI schemas and documentation by using Typescript decorators and metadata.
class User {
@ApiProperty()
declare id: number;
@ApiProperty()
declare name: string;
@ApiProperty({ required: false })
declare mobile?: string;
}
class UsersController {
@ApiOperation({
methods: ["get"],
path: "/users",
summary: "List users"
})
@ApiResponse({ type: [User] })
async list() {
...
}
}
await generateDocument({
controllers: [UsersController],
document: {
info: {
title: "My Api",
version: "1.0.0",
},
},
});Install @martin.xyz/openapi-decorators and reflect-metadata using your favorite package manager.
npm install @martin.xyz/openapi-decoratorsImport reflect-metadata in your main file.
import 'reflect-metadata'
// Rest of your appEnable emitDecoratorMetadata and experimentalDecorators in your tsconfig.json.
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}To get started, you can use the generateDocument function to create an (almost) empty documentation. You can define a base document that will be merged with the generated one.
import 'reflect-metadata'
import { generateDocument } from '@martin.xyz/openapi-decorators'
const document = await generateDocument({
controllers: [],
document: {
info: {
title: 'My API',
version: '1.0.0',
},
},
})
console.log(document) // <- Your generated OpenAPI specificationsA controller is a simple class where each methods could be an Operation. In the following example we have a UsersController which declares an operation GET /users that returns a list of Users.
import { ApiOperation, ApiResponse } from '@martin.xyz/openapi-decorators/decorators'
import User from '../schemas/user'
export default class UsersController {
@ApiOperation({
methods: ['get'],
path: '/users',
summary: 'List users',
})
@ApiResponse({ type: [User] })
async list() {
// ...your logic
}
}In our controller we define the response of your operation to be [User] (a list of users). We now need to create this model.
By using the @ApiProperty decorator on class we can define the properties of our schema.
Unlike other libraries like
@nestjs/swagger, every element of your OpenAPI schema is lazy-loaded. Your models will only be part of your documentation if it is used.
Now that we have our controller ready, we can include it when generating our document.
import 'reflect-metadata'
import { generateDocument } from '@martin.xyz/openapi-decorators'
import UsersController from './controllers/users_controller'
const document = await generateDocument({
controllers: [UsersController],
document: {
info: {
name: 'My API',
version: '1.0.0',
},
},
})
console.log(document) // <- Your generated OpenAPI specificationsDecorators are used to enrich your OpenAPI specifications. They can be applied on a Controller, a Method or a Model. They are all prefixed with Api.
You can find the list of available operators in the source code.
This library brings some utilities to easily display your API documentation using different providers.
import { generateScalarUI } from '@martin.xyz/openapi-decorators/ui'
const html = generateScalarUI('http://localhost:3000/api')import { generateSwaggerUI } from '@martin.xyz/openapi-decorators/ui'
const html = generateSwaggerUI('http://localhost:3000/api')import { generateRapidocUI } from '@martin.xyz/openapi-decorators/ui'
const html = generateRapidocUI('http://localhost:3000/api')A Type Loader transforms a JavaScript Object into a SchemaObject. It gives the ability to extends the inference capabilities when generating a document.
For example String will be transformed into { type: 'string' }.
In this example we will see how to create a custom loader for the Luxon DateTime.
With the following schema we have a property that cannot be loaded as is is unknown by the Document generator.
import { DateTime } from 'luxon'
export default class User {
@ApiProperty()
createdAt: DateTime
}We assume that our API will serialize our DateTime into a string containing the date in ISO format. The user could explicitly define the type @ApiProperty({ type: 'string' }) but as this library goal is to provide a great DX it is possible to create a custom type loader.
import { generateDocument } from '@martin.xyz/openapi-decorators'
import { TypeLoaderFn } from '@martin.xyz/openapi-decorators/types'
import { DateTime } from 'luxon'
const LuxonDateTimeLoader: TypeLoaderFn = (_context, value) => {
if (value === DateTime) {
return { type: 'string' }
}
}
await generateDocument({
loaders: [LuxonDateTimeLoader],
})If you have more complex schemas to generate, you can store it in the components and return a reference instead:
const CustomLoader: TypeLoaderFn = (context, value) => {
if (isCustom(value)) {
const [name, schema] = generateCustomSchema(value)
context.schemas[name] = schema
return { $ref: `#/components/schemas/${name}` }
}
}Decorators does not contain business logic, their purpose is to store metadata to be used when generating the document. This makes it easy to integrate this library into a framework and create custom decorators.
Here is an example of a custom decorator that define an operation summary:
import { OperationMetadataStorage } from '@martin.xyz/openapi-decorators/metadata'
export function ApiSummary(summary: string): MethodDecorator {
return (target, propertyKey) => {
OperationMetadataStorage.mergeMetadata(target, { summary }, propertyKey)
}
}A MetadataStorage is a utility for managing metadata:
This method sets the metadata. It overwrites existing metadata.
import { OperationMetadataStorage } from '@martin.xyz/openapi-decorators/metadata'
// Without propertyKey
OperationMetadataStorage.defineMetadata(target, { summary: 'Hello world' })
// With propertyKey
OperationMetadataStorage.defineMetadata(target, { summary: 'Hello world' }, propertyKey)Similar to defineMetadata, this method sets the metadata but deepmerge its content with exising metadata.
import { OperationMetadataStorage } from '@martin.xyz/openapi-decorators/metadata'
// Without propertyKey
OperationMetadataStorage.mergeMetadata(target, { summary: 'Hello world' })
// With propertyKey
OperationMetadataStorage.mergeMetadata(target, { summary: 'Hello world' }, propertyKey)This method retrieve the stored metadata. When used with a propertyKey you can also define withParent to deepmerge the metadata with the one defined on the class.
You can create a custom metadata storage by using the createMetadataStorage function.
import { createMetadataStorage } from '@martin.xyz/openapi-decorators/metadata'
type CustomMetadata = { foo: 'bar' }
const CustomMetadataKey = Symbol('Custom')
const CustomMetadataStorage = createMetadataStorage<CustomMetadata>(CustomMetadataKey)