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