Skip to content

API Docs

CleanSlice uses Swagger (OpenAPI) to generate interactive API documentation and, more importantly, to export a specification file that powers type-safe SDK generation on the frontend.

Why This Matters

Swagger serves two purposes in CleanSlice:

  1. Interactive documentation at /api for testing endpoints during development.
  2. SDK generation -- the exported swagger-spec.json file feeds @hey-api/openapi-ts to create a fully typed TypeScript client for your frontend.

This means that every controller endpoint and every DTO property must be properly decorated. Missing decorators mean missing types in the generated SDK.

Installation

bash
npm install @nestjs/swagger swagger-ui-express

Setup in main.ts

Swagger configuration lives in main.ts. Here is a complete setup:

typescript
import { NestFactory, Reflector } from '@nestjs/core';
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import * as fs from 'fs';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableCors();
  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
  app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));

  const config = new DocumentBuilder()
    .setTitle('CleanSlice API')
    .setDescription('REST API documentation')
    .setVersion('1.0')
    .addBearerAuth(
      { type: 'http', in: 'header', scheme: 'bearer', bearerFormat: 'JWT' },
      'defaultBearerAuth',
    )
    .addApiKey(
      { type: 'apiKey', name: 'api-key', in: 'header', description: 'API Key' },
      'api-key',
    )
    .build();

  const document = SwaggerModule.createDocument(app, config);

  SwaggerModule.setup('api', app, document, {
    swaggerOptions: { persistAuthorization: true },
  });

  // Export spec for frontend SDK generation
  fs.writeFileSync('swagger-spec.json', JSON.stringify(document));

  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();

The swagger-spec.json is written to disk on every startup. Your frontend project reads this file to generate the SDK.

The operationId Requirement

Every endpoint must include an operationId in its @ApiOperation decorator. The operationId becomes the method name in the generated SDK.

typescript
// With operationId -- SDK generates: client.getUserById({ id })
@ApiOperation({
  summary: 'Get user by ID',
  operationId: 'getUserById',
})

// Without operationId -- SDK generates: client.usersControllerGetUser({ id })
@ApiOperation({
  summary: 'Get user by ID',
})

WARNING

If you omit operationId, the SDK generator falls back to a concatenation of the controller class name and method name. These auto-generated names are ugly and unstable -- they break if you rename a class.

Naming Convention

HTTP MethodPatternExample
GET /usersget{Entities}getUsers
GET /users/:idget{Entity}ByIdgetUserById
POST /userscreate{Entity}createUser
PUT /users/:idupdate{Entity}updateUser
DELETE /users/:iddelete{Entity}deleteUser
POST /users/:id/activate{action}{Entity}activateUser
GET /auth/medescriptive verbme

Controller Decorators

Class-Level

typescript
@ApiTags('Users')                         // Groups endpoints in Swagger UI
@ApiBearerAuth('defaultBearerAuth')       // Marks all endpoints as requiring JWT
@Controller('users')
export class UserController {
  // ...
}

Endpoint-Level

typescript
@ApiOperation({
  summary: 'Create a new user',
  operationId: 'createUser',
})
@ApiResponse({ status: 201, description: 'User created', type: UserDto })
@ApiResponse({ status: 400, description: 'Validation error' })
@ApiResponse({ status: 409, description: 'User already exists' })
@Post()
async createUser(@Body() data: CreateUserDto): Promise<UserDto> {
  return this.userService.createUser(data);
}

DTO Decorators

Every property on a DTO must have an @ApiProperty() or @ApiPropertyOptional() decorator. Without it, the property is invisible to Swagger and missing from the generated SDK types.

typescript
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class UserDto {
  @ApiProperty({ example: 'usr_123abc' })
  id: string;

  @ApiProperty({ example: '[email protected]' })
  email: string;

  @ApiProperty({ example: 'John Doe' })
  name: string;

  @ApiPropertyOptional({ example: '+1234567890' })
  phone?: string;

  @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
  createdAt: Date;
}

TIP

Always include example values. They populate the Swagger UI "Try it out" feature and make the documentation self-documenting.

Custom Response Decorators

CleanSlice provides two custom decorators that standardize how responses appear in the Swagger schema.

ApiSingleResponse

Wraps a single item in the standard { data, success } envelope:

typescript
import { Type, applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiOkResponse, ApiProperty, getSchemaPath } from '@nestjs/swagger';

export class SingleModel<T> {
  public readonly data: T;

  @ApiProperty({ example: true })
  public readonly success: boolean;
}

export const ApiSingleResponse = <TModel extends Type<any>>(model: TModel) => {
  return applyDecorators(
    ApiExtraModels(SingleModel, model),
    ApiOkResponse({
      description: 'Successfully received model',
      schema: {
        allOf: [
          { $ref: getSchemaPath(SingleModel) },
          { properties: { data: { $ref: getSchemaPath(model) } } },
        ],
      },
    }),
  );
};

ApiPaginatedResponse

Wraps a list with pagination metadata:

typescript
import { Type, applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiOkResponse, ApiProperty, getSchemaPath } from '@nestjs/swagger';

export class MetaListDto {
  @ApiProperty({ example: 100 })
  total: number;

  @ApiProperty({ example: 5 })
  lastPage: number;

  @ApiProperty({ example: 1 })
  currentPage: number;

  @ApiProperty({ example: 20 })
  perPage: number;
}

export class PaginationModel<T> {
  public readonly data: T[];

  @ApiProperty()
  public readonly meta: MetaListDto;
}

export const ApiPaginatedResponse = <TModel extends Type<any>>(model: TModel) => {
  return applyDecorators(
    ApiExtraModels(PaginationModel, model),
    ApiOkResponse({
      description: 'Successfully received paginated list',
      schema: {
        allOf: [
          { $ref: getSchemaPath(PaginationModel) },
          { properties: { data: { type: 'array', items: { $ref: getSchemaPath(model) } } } },
        ],
      },
    }),
  );
};

Using Custom Decorators

typescript
@ApiOperation({ summary: 'Get user by ID', operationId: 'getUserById' })
@ApiSingleResponse(UserDto)
@Get(':id')
async getUser(@Param('id') id: string) {
  return this.userService.getUser(id);
}

@ApiOperation({ summary: 'List all users', operationId: 'getUsers' })
@ApiPaginatedResponse(UserDto)
@Get()
async getUsers(@Query() query: FilterUserDto) {
  return this.userService.getUsers(query);
}

Decorator Reference

DecoratorPurposeWhen to Use
@ApiTags()Groups endpoints in Swagger UIEvery controller
@ApiBearerAuth()Marks as requiring JWT authProtected controllers
@ApiOperation()Describes endpoint with operationIdEvery endpoint
@ApiResponse()Documents response status codesEvery endpoint
@ApiBody()Documents request bodyPOST/PUT endpoints
@ApiParam()Documents URL parametersEndpoints with :id etc.
@ApiQuery()Documents query parametersEndpoints with query filters
@ApiProperty()Documents required DTO propertyEvery DTO field
@ApiPropertyOptional()Documents optional DTO propertyOptional DTO fields

What's Next?

  • Controllers -- Full controller pattern with Swagger integration
  • DTOs -- Request and response DTOs with validation and Swagger decorators
  • Getting Started -- Complete main.ts setup

Built with CleanSlice