Skip to main content

Swagger & OpenAPI Documentation

Overview​

The application uses Swagger/OpenAPI for automatic API documentation. This enables interactive API exploration through Swagger UI and provides machine-readable API specifications.

What is Swagger/OpenAPI?​

OpenAPI (formerly Swagger) is:

  • Machine-Readable: API definition in YAML/JSON
  • Interactive Exploration: Built-in Swagger UI
  • Auto-Generated: Driven from code decorators
  • Standardized: Industry-standard API description format
  • Client Generation: Generate SDK clients automatically

Setup in NestJS​

Installation​

pnpm add @nestjs/swagger swagger-ui-express

Enable Swagger​

src/main.ts​

import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

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

// Swagger configuration
const config = new DocumentBuilder()
.setTitle('QMS API')
.setDescription('Quality Management System API Documentation')
.setVersion('1.0.0')
.addServer('http://localhost:3001', 'Development')
.addServer('http://staging-api.example.com', 'Staging')
.addBearerAuth()
.addTag('auth', 'Authentication endpoints')
.addTag('employees', 'Employee management')
.addTag('systems', 'System management')
.addLicense('MIT', 'https://opensource.org/licenses/MIT')
.setContact('Toyota Motor Asia', 'https://example.com', 'support@example.com')
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);

await app.listen(3001);
}

Documenting Endpoints​

Basic Endpoint Documentation​

import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';

@Controller('employees')
@ApiTags('employees')
export class EmployeesController {
@Get()
@ApiOperation({
summary: 'List all employees',
description: 'Returns a paginated list of employees with optional filtering',
})
@ApiResponse({
status: 200,
description: 'Successfully retrieved employees',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - missing or invalid token',
})
async findAll() {
// ...
}
}

Response Documentation​

import { ApiResponse } from '@nestjs/swagger';

@Get(':id')
@ApiResponse({
status: 200,
description: 'Employee found',
schema: {
example: {
data: {
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'John Doe',
email: 'john@example.com',
department: 'Engineering',
createdAt: '2024-01-01T00:00:00.000Z',
},
},
},
})
@ApiResponse({
status: 404,
description: 'Employee not found',
schema: {
example: {
error: {
code: 'NOT_FOUND',
message: 'Employee with ID 123 not found',
},
},
},
})
async findById(@Param('id') id: string) {
// ...
}

Request Body Documentation​

DTO-Based Documentation​

import { IsString, IsEmail, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreateEmployeeDto {
@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'Employee full name',
example: 'John Doe',
minLength: 1,
maxLength: 255,
})
name: string;

@IsEmail()
@ApiProperty({
description: 'Employee email address',
example: 'john@example.com',
})
email: string;

@IsString()
@ApiProperty({
description: 'Department name',
example: 'Engineering',
enum: ['Engineering', 'Sales', 'Marketing', 'HR'],
})
department: string;

@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'Job role',
example: 'Senior Developer',
})
role: string;

@ApiProperty({
description: 'Phone number (optional)',
example: '+1234567890',
required: false,
})
phone?: string;
}

@Post()
@ApiOperation({ summary: 'Create new employee' })
@ApiResponse({
status: 201,
description: 'Employee created successfully',
type: EmployeeDTO,
})
async create(@Body() createEmployeeDto: CreateEmployeeDto) {
// ...
}

Query Parameters Documentation​

import { ApiQuery } from '@nestjs/swagger';

@Get()
@ApiQuery({
name: 'page',
type: Number,
description: 'Page number (1-indexed)',
example: 1,
required: false,
})
@ApiQuery({
name: 'limit',
type: Number,
description: 'Results per page',
example: 20,
required: false,
})
@ApiQuery({
name: 'department',
type: String,
description: 'Filter by department',
example: 'Engineering',
required: false,
})
async findAll(
@Query('page') page: number,
@Query('limit') limit: number,
@Query('department') department?: string,
) {
// ...
}

Parameter Documentation​

import { ApiParam } from '@nestjs/swagger';

@Get(':id')
@ApiParam({
name: 'id',
description: 'Employee ID (UUID)',
example: '123e4567-e89b-12d3-a456-426614174000',
})
async findById(@Param('id') id: string) {
// ...
}

Authentication Documentation​

Bearer Token​

import { ApiBearerAuth } from '@nestjs/swagger';

@Controller('employees')
@ApiBearerAuth() // Mark controller as requiring bearer token
@UseGuards(JwtAuthGuard)
export class EmployeesController {
@Get()
@ApiOperation({ summary: 'Get all employees' })
async findAll() {
// ...
}
}

Optional Authentication​

@Get('public')
@ApiOperation({ summary: 'Public endpoint' })
@ApiResponse({ status: 200, description: 'No auth required' })
async publicEndpoint() {
// ...
}

@Get('protected')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiResponse({ status: 401, description: 'Unauthorized' })
async protectedEndpoint() {
// ...
}

Example API Documentation​

Complete Endpoint​

import {
Controller,
Get,
Post,
Patch,
Delete,
Param,
Body,
Query,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiQuery,
ApiParam,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { EmployeesService } from './employees.service';
import { CreateEmployeeDto } from './dto/create-employee.dto';
import { UpdateEmployeeDto } from './dto/update-employee.dto';

@Controller('employees')
@ApiTags('employees')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
export class EmployeesController {
constructor(private readonly employeesService: EmployeesService) {}

@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({
summary: 'Create new employee',
description: 'Creates a new employee record. Requires admin or manager role.',
})
@ApiResponse({
status: 201,
description: 'Employee created successfully',
schema: {
example: {
data: {
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'John Doe',
email: 'john@example.com',
department: 'Engineering',
createdAt: '2024-01-01T00:00:00.000Z',
},
},
},
})
@ApiResponse({
status: 400,
description: 'Bad request - validation error',
schema: {
example: {
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: [
{
field: 'email',
message: 'Invalid email format',
},
],
},
},
},
})
@ApiResponse({
status: 409,
description: 'Conflict - email already exists',
})
async create(@Body() createEmployeeDto: CreateEmployeeDto) {
return this.employeesService.create(createEmployeeDto);
}

@Get()
@ApiOperation({
summary: 'List employees',
description: 'Returns a paginated list of employees',
})
@ApiQuery({
name: 'page',
type: Number,
required: false,
example: 1,
})
@ApiQuery({
name: 'limit',
type: Number,
required: false,
example: 20,
})
@ApiQuery({
name: 'department',
type: String,
required: false,
description: 'Filter by department',
})
@ApiResponse({
status: 200,
description: 'List of employees',
schema: {
example: {
data: [
{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'John Doe',
email: 'john@example.com',
department: 'Engineering',
},
],
meta: {
page: 1,
limit: 20,
total: 100,
totalPages: 5,
},
},
},
})
async findAll(
@Query('page') page: number = 1,
@Query('limit') limit: number = 20,
@Query('department') department?: string,
) {
return this.employeesService.findAll({ page, limit, department });
}

@Get(':id')
@ApiParam({
name: 'id',
type: String,
description: 'Employee UUID',
})
@ApiOperation({ summary: 'Get employee by ID' })
@ApiResponse({
status: 200,
description: 'Employee found',
})
@ApiResponse({
status: 404,
description: 'Employee not found',
})
async findById(@Param('id') id: string) {
return this.employeesService.findById(id);
}

@Patch(':id')
@ApiParam({
name: 'id',
type: String,
description: 'Employee UUID',
})
@ApiOperation({ summary: 'Update employee' })
@ApiResponse({
status: 200,
description: 'Employee updated',
})
@ApiResponse({
status: 404,
description: 'Employee not found',
})
async update(@Param('id') id: string, @Body() updateEmployeeDto: UpdateEmployeeDto) {
return this.employeesService.update(id, updateEmployeeDto);
}

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiParam({
name: 'id',
type: String,
description: 'Employee UUID',
})
@ApiOperation({ summary: 'Delete employee' })
@ApiResponse({
status: 204,
description: 'Employee deleted',
})
@ApiResponse({
status: 404,
description: 'Employee not found',
})
async delete(@Param('id') id: string) {
return this.employeesService.delete(id);
}
}

Custom Schemas​

Response DTO for Swagger​

import { ApiProperty } from '@nestjs/swagger';

export class EmployeeResponseDto {
@ApiProperty({
example: '123e4567-e89b-12d3-a456-426614174000',
description: 'Unique employee identifier',
})
id: string;

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

@ApiProperty({
example: 'john@example.com',
description: 'Email address',
})
email: string;

@ApiProperty({
example: 'Engineering',
enum: ['Engineering', 'Sales', 'Marketing', 'HR'],
})
department: string;

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

export class EmployeeListResponseDto {
@ApiProperty({
type: [EmployeeResponseDto],
})
data: EmployeeResponseDto[];

@ApiProperty({
example: {
page: 1,
limit: 20,
total: 100,
totalPages: 5,
},
})
meta: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}

Swagger UI Customization​

Custom CSS​

const config = new DocumentBuilder()
.setTitle('QMS API')
.setDescription('Quality Management System')
.setVersion('1.0.0')
.build();

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

SwaggerModule.setup('api/docs', app, document, {
swaggerOptions: {
persistAuthorization: true,
docExpansion: 'list',
defaultModelsExpandDepth: 1,
},
customCss: `
.topbar {
background-color: #1a1a1a;
}
.info .title {
color: #2e8555;
}
`,
customfavIcon: 'https://example.com/favicon.ico',
});

OpenAPI JSON Export​

Access the full OpenAPI specification:

# Get JSON spec
curl http://localhost:3001/api/docs-json

# Get YAML spec
curl http://localhost:3001/api/docs-yaml

Best Practices​

1. Document Everything​

// βœ… GOOD: Comprehensive documentation
@Post()
@ApiOperation({
summary: 'Create employee',
description: 'Creates a new employee record in the system',
})
@ApiResponse({
status: 201,
description: 'Employee created successfully',
})

// ❌ AVOID: No documentation
@Post()
async create(@Body() dto) {}

2. Use Descriptive Names​

// βœ… GOOD
@ApiProperty({
description: 'Employee full name',
})
name: string;

// ❌ AVOID
@ApiProperty()
name: string;

3. Provide Examples​

// βœ… GOOD
@ApiProperty({
example: 'john@example.com',
description: 'Email address',
})
email: string;

// ❌ AVOID
@ApiProperty({
description: 'Email address',
})
email: string;

4. Document Error Responses​

// βœ… GOOD: All response codes documented
@ApiResponse({ status: 200, description: 'Success' })
@ApiResponse({ status: 400, description: 'Validation error' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 404, description: 'Not found' })

// ❌ AVOID: Only success documented
@ApiResponse({ status: 200, description: 'Success' })

Generating Client SDKs​

Use tools to auto-generate client libraries from OpenAPI spec:

# Generate TypeScript client
npx openapi-generator-cli generate \
-i http://localhost:3001/api/docs-json \
-g typescript-axios \
-o ./generated/client

# Generate Python client
npx openapi-generator-cli generate \
-i http://localhost:3001/api/docs-json \
-g python \
-o ./generated/python-client

Resources​