nest-openapi
v1.0.12
Published
Auto generate openapi spec from nest.js codebase with single command
Maintainers
Readme
nest-openapi
💡 If you use Nest.js with class-validator, this package is for you.
Motivation
😇 Life is too short to write API docs manually.
I was tried of using bunch of nest.js swagger decorators for auto generating openapi spec. Code looked ugly. Too much extra code just to get the basic openapi structure.
I looked for a solution that could generate the spec just by looking into the nest.js codebase. I couldn't find any so I built one.
What it does
{
"openapi": "3.1.0",
"info": {
"title": "User API",
"version": "1.0.0",
"description": "An example API to create a user, optionally with a referrer ID."
},
"servers": [{ "url": "https://api.example.com/v1" }],
"paths": {
"/users/{referrerId}": {
"post": {
"summary": "Create a new user",
"operationId": "createUser",
"tags": ["Users"],
"parameters": [{ "$ref": "#/components/parameters/ReferrerId" }],
"requestBody": {
"$ref": "#/components/requestBodies/CreateUserRequest"
},
"responses": {
"201": { "$ref": "#/components/responses/UserCreated" },
"400": { "$ref": "#/components/responses/BadRequest" }
}
}
}
},
"components": {
"parameters": {
"ReferrerId": {
"name": "referrerId",
"in": "path",
"required": true,
"description": "The ID of the user who referred the new user",
"schema": { "type": "string", "example": "ref123" }
}
},
"schemas": {
"User": {
"type": "object",
"properties": {
"id": { "type": "string", "example": "abc123" },
"name": { "type": "string", "example": "John Doe" },
"email": {
"type": "string",
"format": "email",
"example": "[email protected]"
},
"createdAt": {
"type": "string",
"format": "date-time",
"example": "2025-01-01T12:00:00Z"
},
"referrerId": {
"type": "string",
"nullable": true,
"example": "ref123"
}
},
"required": ["id", "name", "email", "createdAt"]
},
"CreateUserInput": {
"type": "object",
"properties": {
"name": { "type": "string", "example": "John Doe" },
"email": {
"type": "string",
"format": "email",
"example": "[email protected]"
},
"password": {
"type": "string",
"format": "password",
"example": "P@ssw0rd!"
}
},
"required": ["name", "email", "password"]
},
"Error": {
"type": "object",
"properties": {
"message": { "type": "string", "example": "Invalid request payload" }
}
}
},
"requestBodies": {
"CreateUserRequest": {
"description": "User creation request payload",
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CreateUserInput" }
}
}
}
},
"responses": {
"UserCreated": {
"description": "User successfully created",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/User" }
}
}
},
"BadRequest": {
"description": "Invalid request payload",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Error" }
}
}
}
}
}
}How to use
Generate openapi spec with single command
> pnpm add nest-openapi -g
> nest-openapiLive preview for development
nest-openapi -p 8000CLI options
| option | default value | | ------------ | ------------- | | -o, --output | openapi.json | | -p, --port | 8080 |
Basic patterns to follow.
Just need to maintain the codebase and follow some basic patterns. The package will handle the rest.
- Class name in whole project must be unique. Better to use prefix and suffix with class names.
- While using nest.js decorators i.e.
@Param,@Body,@Query, etc. always use class as type annotation and add validations usingclass-validator. - Define return type annotation of the endpoint with separate class. It can be a regular class, I'm just used to writing code like below.
Here is the code example
export class ProductIdDto {
@IsMongoId()
productId: string
}
export class ProductUpdateDto {
@IsNotEmpty()
@IsString()
name: string
@IsNotEmpty()
@IsString()
description: string
}
export class ProductSerializer {
@Expose() id: string
@Expose() name: string
@Expose() description: string
@Expose() @Type(() => Date) createdAt: string
@Expose() @Type(() => Date) updatedAt: string
}
@Controller('products')
export class ProductController {
@Patch(':productId')
async updateProduct(
@Body() productUpdateDto: ProductUpdateDto,
@Param() { productId }: ProductIdDto,
): Promise<ProductSerializer> {
// logic here
console.log({ productId })
return plainToInstance(ProductSerializer, productUpdateDto, {
strategy: 'excludeAll',
})
}
}How it works
- To navigate through typescript AST it uses
ts-morph. It looks for classes with@Controllerdecorator and loops over the methods. - To generate example for the schemas it uses
@faker-js/fakerwithjson-schema-faker - To turn dto into schema it uses
class-validator-jsonschema, since it supportsclass-validatorvalidation decorators. - To turn response class to schema it uses
ts-json-schema-generator. - It uses controller class name and method name for operation id
{controllerClassName}-{methodName}
customization
With config file
Create nest-openapi.config.ts file in the project root directory.
import { Config } from 'nest-openapi'
const config: Config = {
extends: {
servers: [
{ url: 'https://dev.example.com', description: 'Development' },
{ url: 'https://example.com', description: 'Production' },
],
components: {
/** responses below will be included in every operations */
responses: {
'401': {
description: 'Unauthorized',
},
'402': {
description: 'Payment required',
},
'500': {
description: 'Server error',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
error: {
type: 'string',
example: 'Server error',
},
message: {
type: 'string',
example: 'Internal server error',
},
},
},
},
},
},
},
},
},
/** glob pattern for typescript AST */
glob: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/**/*.e2e-spec.ts'],
}
export default configWith comments
use js-doc
@tagand@descriptionto have custom tag name and description./** * @tag Users * @description IT contains APIs to work with users. */ @Controller() export class UserController {}use
@summary,@description,@deprecatedin endpoints./** * @summary Method name * @description Use this endpoint do the the task. * @deprecated This endpoint is deprecated, use [this](#tag/className-methodName) endpoint. */ @Patch() async methodName() {}use
@descriptionin the properties of the class.class Dto { /** @description This is a description. */ property: string }
