-
Notifications
You must be signed in to change notification settings - Fork 4
NSC Events API Documentation Guide
This guide is designed for new team members joining the NSC Events project. By the end of this document, you will understand:
- How our API documentation is generated and served
- The decorators and patterns we use to document endpoints
- How to add documentation when creating new endpoints
- Common troubleshooting steps for documentation issues
We use OpenAPI (via Swagger) for several important reasons:
-
Single Source of Truth: The API documentation lives in the code itself. When you update an endpoint, the documentation updates automaticallyβno separate wiki to maintain.
-
Interactive Testing: Swagger UI provides a "Try it out" feature. You can test endpoints directly in your browser without needing Postman or curl commands.
-
Frontend-Backend Alignment: Frontend developers can explore the exact request/response shapes expected by each endpoint, reducing miscommunication.
-
Onboarding Acceleration: New developers can explore the entire API surface within minutes of cloning the repo.
-
JWT Authentication Testing: Our Swagger setup includes "Authorize" functionalityβyou can paste a JWT token once and test authenticated endpoints seamlessly.
We use a Code-First approach to API documentation:
flowchart LR
A[TypeScript Decorators] --> B[NestJS Scanner]
B --> C[OpenAPI 3.0 Spec]
C --> D[Swagger UI]
Here's what happens when you start the server:
-
TypeScript Decorators: You annotate controllers, methods, and DTOs with decorators like
@ApiOperation(),@ApiResponse(), and@ApiProperty(). -
NestJS Scanner: On application startup, NestJS scans all registered modules and extracts decorator metadata.
-
OpenAPI Spec: The
SwaggerModulecompiles this metadata into a JSON document following the OpenAPI 3.0 specification. -
Swagger UI: This JSON spec is served through an interactive web interface where you can explore and test the API.
Once the backend server is running, you can access:
| Resource | URL |
|---|---|
| Swagger UI | http://localhost:3000/api/docs |
| Raw OpenAPI JSON | http://localhost:3000/api/docs-json |
Note: The port may vary if you've configured a different
PORTenvironment variable.
Currently, Swagger is enabled in all environments (development, test, and production). This is configured in src/main.ts:
// Swagger configuration (from src/main.ts)
const config = new DocumentBuilder()
.setTitle('NSC Events API')
.setDescription('API documentation for NSC Events management system')
.setVersion('2.0.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
},
'JWT-auth',
)
.addTag('Authentication', 'User authentication and authorization endpoints')
.addTag('Users', 'User management endpoints')
.addTag('Events', 'Event/Activity management endpoints')
.addTag('Event Registration', 'Event registration and attendance endpoints')
.addTag('Google Auth', 'Google Calendar integration endpoints')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document, {
swaggerOptions: {
persistAuthorization: true, // Keeps your JWT token across page refreshes
tagsSorter: 'alpha', // Sorts API tags alphabetically
operationsSorter: 'alpha', // Sorts operations alphabetically
},
customSiteTitle: 'NSC Events API Documentation',
});TODO: Consider disabling Swagger in production for security. You could wrap the Swagger setup in a condition like:
if (process.env.NODE_ENV !== 'production') { // Swagger setup here }
Every controller should have these decorators at the class level:
Groups all endpoints in this controller under a labeled section in Swagger UI.
// From src/auth/controllers/auth.controller.ts
@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
// ...
}// From src/activity/controllers/activity/activity.controller.ts
@ApiTags('Events')
@Controller('events')
export class ActivityController {
// ...
}When a controller or endpoint requires JWT authentication, use this decorator. The string 'JWT-auth' must match the security scheme name defined in main.ts.
// From src/user/controllers/user/user.controller.ts
@ApiTags('Users')
@ApiBearerAuth('JWT-auth') // All endpoints in this controller require auth
@UseGuards(AuthGuard('jwt'))
@Controller('users')
export class UserController {
// ...
}For individual endpoints within an otherwise public controller:
// From src/auth/controllers/auth.controller.ts
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth('JWT-auth') // Only this endpoint requires auth
@Post('/change-password')
async changePassword(...) { }Each endpoint should be documented with these decorators:
Provides a summary and description for the endpoint.
// From src/auth/controllers/auth.controller.ts
@Post('/signup')
@ApiOperation({
summary: 'Register a new user',
description: 'Creates a new user account and returns a JWT token',
})
signUp(@Body() signUpDto: SignUpDto): Promise<{ token: string }> {
return this.authService.register(signUpDto);
}Document all possible response codes, including success and error cases.
// From src/auth/controllers/auth.controller.ts
@Post('/signup')
@ApiResponse({
status: 201,
description: 'User successfully registered',
schema: {
type: 'object',
properties: {
token: {
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
},
})
@ApiResponse({ status: 400, description: 'Invalid input data' })
@ApiResponse({ status: 409, description: 'User already exists' })// From src/activity/controllers/activity/activity.controller.ts
@Get('find/:id')
@ApiParam({ name: 'id', description: 'Event ID (UUID)' })
async findActivityById(@Param('id') id: string): Promise<Activity> {
return this.activityService.getActivityById(id);
}// From src/activity/controllers/activity/activity.controller.ts
@Get('')
@ApiQuery({
name: 'page',
required: false,
description: 'Page number',
example: '1',
})
@ApiQuery({
name: 'tags',
required: false,
description: 'Filter by tags (comma-separated)',
example: 'club,study',
})
async getAllActivities(
@Query('page') page = '1',
@Query('tags') tagsParam?: string,
): Promise<Activity[]> { }// From src/auth/controllers/auth.controller.ts
@Post('/login')
@ApiBody({ type: LoginDto })
login(@Body() loginDto: LoginDto): Promise<{ token: string }> {
return this.authService.login(loginDto);
}// From src/activity/controllers/activity/activity.controller.ts
@Post('attend/:id')
@ApiOperation({
summary: 'Attend event (DEPRECATED)',
description: 'DEPRECATED: Use /api/event-registration/attend instead',
deprecated: true,
})
async attendEvent(...) { }The Golden Rule: If a property isn't decorated with @ApiProperty(), it won't appear in Swagger.
// From src/auth/dto/signup.dto.ts
export class SignUpDto {
@ApiProperty({
description: 'User first name',
example: 'John',
})
@IsNotEmpty()
@IsString()
readonly firstName: string;
@ApiProperty({
description: 'User email address',
example: 'john.doe@example.com',
})
@IsNotEmpty()
@IsEmail({}, { message: 'Please enter correct email' })
readonly email: string;
}// From src/user/dto/update-user.dto.ts
export class UpdateUserDto {
@ApiProperty({
description: 'User first name',
example: 'John',
required: false, // Mark as optional in Swagger
})
@IsOptional() // Validation: field is optional
@IsString()
readonly firstName: string;
}// From src/auth/dto/login.dto.ts
@ApiProperty({
description: 'User password (minimum 8 characters)',
example: 'Password123!',
minLength: 8,
})
@IsNotEmpty()
@IsString()
@MinLength(8)
readonly password: string;// From src/auth/dto/signup.dto.ts
@ApiProperty({
description: 'User role',
enum: Role,
example: Role.user,
})
@IsNotEmpty()
@IsString()
readonly role: Role;Where Role is defined as:
// From src/user/entities/user.entity.ts
export enum Role {
admin = 'admin',
creator = 'creator',
user = 'user',
}// From src/event-registration/dto/create-event-registration.dto.ts
@ApiProperty({
description: 'Whether the user has attended the event',
example: false,
default: false,
required: false,
})
@IsBoolean()
@IsOptional()
isAttended = false;For endpoints that accept file uploads, we use special decorators:
// From src/activity/controllers/activity/activity.controller.ts
@Post('new')
@UseGuards(AuthGuard())
@ApiBearerAuth('JWT-auth')
@UseInterceptors(FileInterceptor('coverImage'))
@ApiConsumes('multipart/form-data')
@ApiOperation({
summary: 'Create new event',
description: 'Creates a new event. Supports file upload for cover image.',
})// From src/activity/controllers/activity/activity.controller.ts
@Put(':id/cover-image')
@UseInterceptors(FileInterceptor('coverImage'))
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
coverImage: {
type: 'string',
format: 'binary',
description: 'Cover image file',
},
},
required: ['coverImage'],
},
})
async uploadCoverImage(...) { }For complex forms with both file and text fields:
// From src/activity/controllers/activity/activity.controller.ts (create event)
@ApiBody({
schema: {
type: 'object',
properties: {
eventTitle: { type: 'string', example: 'Tech Workshop 2025' },
eventDescription: { type: 'string', example: 'A workshop on modern web technologies' },
startDate: { type: 'string', format: 'date-time', example: '2025-12-01T14:00:00Z' },
coverImage: {
type: 'string',
format: 'binary',
description: 'Cover image file (optional)',
},
// ... more fields
},
required: ['eventTitle', 'eventDescription', 'startDate', /* ... */],
},
})TODO: The following DTOs have validation decorators but are missing
@ApiProperty()decorators. These should be added for complete Swagger documentation:
src/activity/dto/create-activity.dto.tssrc/activity/dto/update-activity.dto.tsThese DTOs use inline
@ApiBody({ schema: {...} })in the controller as a workaround, but ideally the DTOs themselves should be fully decorated.
There's an important relationship between validation decorators (from class-validator) and documentation decorators (from @nestjs/swagger). They should tell the same story.
// From src/auth/dto/login.dto.ts
export class LoginDto {
@ApiProperty({
description: 'User email address',
example: 'user@example.com',
})
@IsNotEmpty() // Validation: required
@IsEmail({}, { message: 'Please enter correct email' }) // Validation: must be email
readonly email: string;
@ApiProperty({
description: 'User password (minimum 8 characters)',
example: 'Password123!',
minLength: 8, // Documentation: minimum length
})
@IsNotEmpty() // Validation: required
@IsString() // Validation: must be string
@MinLength(8) // Validation: minimum 8 characters
readonly password: string;
}Key alignment points:
| Validation Decorator | Corresponding @ApiProperty Option |
|---|---|
@IsNotEmpty() |
required: true (default) |
@IsOptional() |
required: false |
@MinLength(n) |
minLength: n |
@MaxLength(n) |
maxLength: n |
@IsEnum(Enum) |
enum: Enum |
@IsEmail() |
Consider adding format: 'email'
|
When validation and documentation are misaligned:
- Frontend developers may send invalid data because docs said it was optional
- Users get confusing error messages
- Testing becomes unreliable
Use this checklist when adding a new endpoint:
- Identify which controller this endpoint belongs to
- Determine if it requires authentication
- Design the request/response DTOs
- Add
@ApiTags('YourModule')to the controller class (if not already present) - Add
@ApiBearerAuth('JWT-auth')if the endpoint requires authentication
- Add
@ApiOperation({ summary: '...', description: '...' }) - Add
@ApiResponse()for the success case (200, 201, etc.) with response type/schema - Add
@ApiResponse()for each error case:-
400- Bad Request (validation errors) -
401- Unauthorized (missing/invalid token) -
403- Forbidden (insufficient permissions) -
404- Not Found -
409- Conflict (duplicate resource)
-
- Add
@ApiParam()for each URL parameter - Add
@ApiQuery()for each query parameter - Add
@ApiBody()referencing your request DTO
- Add
@ApiProperty()to every property in your request DTO - Add
@ApiProperty()to every property in your response DTO (if returning a class) - Include
description,example, andrequired(for optional fields) - For enums, add
enum: YourEnum
- Ensure
class-validatordecorators align with@ApiPropertyconstraints - Test that validation errors are returned correctly
- Restart the NestJS server
- Open Swagger UI at
http://localhost:3000/api/docs - Verify your endpoint appears under the correct tag
- Verify request body schema is correct
- Verify response schemas are correct
- Test the endpoint using "Try it out"
-
Is the controller registered in a module?
// your.module.ts @Module({ controllers: [YourController], // Must be listed here }) export class YourModule {}
-
Is the module imported in
AppModule?// app.module.ts @Module({ imports: [YourModule], // Must be imported here }) export class AppModule {}
-
Check for typos in route decorators
-
@Controller('events')β endpoint will be at/api/events -
@Get('find/:id')β endpoint will be at/api/events/find/:id
-
-
Restart the server
- Swagger documentation is generated at startup. Changes require a restart.
-
Did you add
@ApiProperty()to each property?// WRONG - property won't appear @IsString() readonly name: string; // CORRECT - property will appear @ApiProperty({ description: 'User name' }) @IsString() readonly name: string;
-
Are you using an interface instead of a class?
// WRONG - interfaces have no runtime metadata interface CreateUserDto { name: string; } // CORRECT - classes preserve metadata class CreateUserDto { @ApiProperty() name: string; }
-
Check that decorators align
- If
@IsNotEmpty()is present, don't setrequired: falsein@ApiProperty() - If
@MinLength(8)is present, addminLength: 8to@ApiProperty()
- If
-
Ensure
ValidationPipeis enabled globally// src/main.ts app.useGlobalPipes(new ValidationPipe());
-
Did you add
@ApiConsumes('multipart/form-data')?@Post('upload') @ApiConsumes('multipart/form-data') // Required for file uploads @UseInterceptors(FileInterceptor('file'))
-
Did you use
@ApiBody()with binary format?@ApiBody({ schema: { type: 'object', properties: { file: { type: 'string', format: 'binary', // This tells Swagger it's a file }, }, }, })
- Click the "Authorize" button at the top of Swagger UI
- Enter your JWT token (without the "Bearer " prefixβSwagger adds it automatically)
- Click "Authorize"
- The lock icons should now show as locked for authenticated endpoints
Our API is organized into these sections:
| Tag | Description | Controller |
|---|---|---|
| Authentication | User registration, login, password management | AuthController |
| Users | User profile management (admin functions) | UserController |
| Events | Event/Activity CRUD operations | ActivityController |
| Event Registration | Event registration and attendance | EventRegistrationController |
| Google Auth | Google Calendar integration | GoogleAuthController |
- NestJS OpenAPI Documentation - Official NestJS Swagger guide
- OpenAPI 3.0 Specification - Full specification reference
- class-validator Documentation - Validation decorators reference
- Swagger UI Documentation - Understanding the Swagger interface
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiParam,
ApiQuery,
ApiBody,
ApiConsumes,
ApiProperty,
} from '@nestjs/swagger';@ApiTags('Example')
@Controller('example')
export class ExampleController {
@Get(':id')
@ApiOperation({ summary: 'Get item by ID' })
@ApiParam({ name: 'id', description: 'Item ID' })
@ApiResponse({ status: 200, description: 'Item found' })
@ApiResponse({ status: 404, description: 'Item not found' })
getItem(@Param('id') id: string) {
// implementation
}
}export class CreateItemDto {
@ApiProperty({
description: 'Item name',
example: 'My Item',
})
@IsNotEmpty()
@IsString()
name: string;
@ApiProperty({
description: 'Item description',
example: 'A detailed description',
required: false,
})
@IsOptional()
@IsString()
description?: string;
}Home β’ New Student Onboarding β’ Guides β’ Projects β’ Code of Conduct β’ FAQ
Last updated: 12/7/2025