Skip to content

Cell - OpenAPI Component ​

Add @celljs/openapi to the dependencies to document the API of your @celljs/mvc controllers: the application serves an OpenAPI 3.0 document at /openapi.json and Swagger UI at /api-docs. The document is built from the routes themselves (paths, @Param / @Query / @Header / @Cookie / @Body, TypeScript parameter and return types), so it stays in step with the code; @Api* decorators add what types cannot say. Example: examples/openapi-app.

ts
import { Body, Controller, Get, Param, Post, Query } from '@celljs/mvc/lib/node';
import { ApiOperation, ApiProperty, ApiPropertyOptional, ApiResponse, ApiTags } from '@celljs/openapi';
import { IsInt, IsOptional, IsString, Min } from 'class-validator';

export class CreatePet {
    @IsString()                       // class-validator decorators become the schema: type, required, enum, length, range...
    @ApiProperty({ example: 'Tom' })  // ...and @ApiProperty adds a description, an example or the type of an array
    name: string;

    @IsInt() @Min(0) @IsOptional()
    age?? number;
}

export class Pet extends CreatePet {
    @ApiProperty()
    id: number;
}

@Controller('pets')
@ApiTags('Pets')
export class PetController {
    @Get()
    @ApiResponse({ type: [Pet] })    // async methods and arrays need the type: TypeScript emits only Promise / Array
    list(@Query() query: PetQuery): Promise<Pet[]> { ... }

    @Get(':id')
    @ApiOperation({ summary: 'Find a pet' })
    @ApiResponse({ status: 404, description: 'No such pet' })
    get(@Param('id') id: number): Pet { ... }   // the return type is the 200 response

    @Post()
    create(@Body() pet: CreatePet): Pet { ... } // the class of the body is the request schema
}

What goes into the document:

  • Paths: every route of a controller, :id as {id}; an optional part (/users(/:id)) gives one path with it and one without. Routes with a RegExp path are left out.
  • Parameters: @Param('id'), @Query('q'), @Header('x-name'), @Cookie('name') with the type of the parameter; @Query() query: PetQuery gives one query parameter per property of the class. Path parameters missing from the method are added as strings.
  • Body: @Body() pet: CreatePet as an application/json body; named @Body('name') parameters as the properties of an object.
  • Responses: the @ApiResponse ones (on the class for all its methods, or on the method); without a 2xx one, 200 with the return type. The content type follows the view: JSON by default, text/plain for @Text(), text/html for @Html().
  • Schemas: each class becomes an entry of components.schemas, referred to by $ref. Its properties are the ones with class-validator decorators (read from class-validator's metadata when it is installed, e.g. with @celljs/validation; it is not a dependency of this package) or with @ApiProperty. A property is required unless it has @IsOptional() or @ApiPropertyOptional().
  • Tags: @ApiTags, otherwise the controller name without Controller.
DecoratorOnPurpose
@ApiTags(...tags)class, methodGroups operations
@ApiOperation({ summary, description, operationId, deprecated })methodDescribes an operation; operationId defaults to <Class>_<method>
@ApiResponse({ status, description, type, schema, contentType })class, methodA response; repeat it per status; type: [Pet] for a list
@ApiProperty(options) / @ApiPropertyOptional(options)propertyA property of a request or response class; type, enum (a list or a TypeScript enum), description, example, format and any other schema field
@ApiParam / @ApiQuery / @ApiHeader({ name, ... })methodDescribes a parameter, or documents one the method reads from the request itself
@ApiSecurity(name, scopes?) / @ApiBearerAuth()class, methodRequires a security scheme; bearer is declared for @ApiBearerAuth() when components.securitySchemes has none
@ApiExclude()class, methodLeaves it out of the document

Configuration ​

yaml
openapi:
  enabled: true                   # false: serve neither the document nor the UI
  url: /openapi.json              # under server.path
  ui:
    enabled: true
    url: /api-docs
    cdn: https://cdn.jsdelivr.net/npm/swagger-ui-dist@5   # where the page loads Swagger UI from
  document:                       # merged into the generated document
    openapi: 3.0.3
    info:
      title: Pet Store
      version: 1.0.0
    servers:
      - url: https://api.example.com
    components:
      securitySchemes:
        apiKey: { type: apiKey, in: header, name: X-API-Key }

The document and the page answer GET requests no controller answered, so a route of the application with the same path wins and other requests pay nothing. They are public unless @celljs/security protects them; to hide them in production, set enabled: false in the production config (cell.production.yml).

To write the document to a file without starting a server (for API clients, code generators or a review), fetch it from createTestApp() of @celljs/cli, as scripts/export-openapi.ts of the example does.

OpenApiDocumentProvider (@celljs/openapi/lib/node) gives the document in code; rebind it to change how it is built.