- Updated skill documentation files to include structured metadata for better organization. - Enhanced bilingual descriptions and guidelines for clarity in both English and Vietnamese. - Refined sections on usage, best practices, and related skills to ensure consistency across all documentation. - Improved formatting and removed outdated references to streamline the documentation experience. - Added best practices checklists to relevant skills for better usability and adherence to standards.
10 KiB
10 KiB
name, description
| name | description |
|---|---|
| api-design | RESTful API design standards for GoodGo microservices. Use when creating new API endpoints, designing DTOs, implementing controllers, writing OpenAPI documentation, or standardizing API responses. |
RESTful API Design Standards
When to Use This Skill
Use this skill when:
- Creating new API endpoints
- Designing request/response DTOs
- Implementing controllers and routes
- Writing OpenAPI/Swagger documentation
- Standardizing error responses
- Implementing pagination, filtering, and sorting
- Setting up API versioning
- Designing resource relationships
Core Principles
- Consistency: All APIs follow the same patterns
- Predictability: Developers can guess endpoint behavior
- Simplicity: Easy to understand and use
- Documentation: Self-documenting through OpenAPI
- Error Handling: Clear, actionable error messages
URL Structure
https://api.goodgo.com/v1/{resource}/{id}/{sub-resource}
Examples:
GET /v1/users # List users
POST /v1/users # Create user
GET /v1/users/123 # Get user by ID
PUT /v1/users/123 # Update user
DELETE /v1/users/123 # Delete user
GET /v1/users/123/orders # Get user's orders
POST /v1/users/123/orders # Create order for user
HTTP Methods
- GET: Retrieve resource(s) - Safe, Idempotent
- POST: Create new resource - Not idempotent
- PUT: Full update - Idempotent
- PATCH: Partial update - Idempotent
- DELETE: Remove resource - Idempotent
Standard Response Format
Success Response
interface SuccessResponse<T> {
success: true;
data: T;
metadata?: {
timestamp: string;
version: string;
requestId: string;
};
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// Example
{
"success": true,
"data": {
"id": "123",
"email": "user@example.com",
"name": "John Doe"
},
"metadata": {
"timestamp": "2024-01-01T00:00:00Z",
"version": "1.0.0",
"requestId": "req_abc123"
}
}
Error Response
interface ErrorResponse {
success: false;
error: {
code: string;
message: string;
details?: any;
field?: string;
stack?: string; // Only in development
};
metadata?: {
timestamp: string;
requestId: string;
};
}
// Example
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"field": "email",
"details": {
"provided": "invalid-email",
"expected": "valid email address"
}
}
}
Status Codes
// Success codes
200 OK // GET, PUT, PATCH success
201 Created // POST success with resource creation
204 No Content // DELETE success
// Client errors
400 Bad Request // Invalid request data
401 Unauthorized // Missing/invalid authentication
403 Forbidden // Valid auth but no permission
404 Not Found // Resource doesn't exist
409 Conflict // Resource conflict (duplicate)
422 Unprocessable // Validation errors
// Server errors
500 Internal Error // Unexpected server error
502 Bad Gateway // External service error
503 Service Unavailable // Service temporarily down
504 Gateway Timeout // External service timeout
DTOs (Data Transfer Objects)
Request DTOs
// create.dto.ts
import { IsEmail, IsNotEmpty, IsOptional, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
@IsNotEmpty()
email: string;
@MinLength(6)
@IsNotEmpty()
password: string;
@IsOptional()
name?: string;
}
// update.dto.ts
export class UpdateUserDto {
@IsEmail()
@IsOptional()
email?: string;
@IsOptional()
name?: string;
@IsOptional()
avatar?: string;
}
// query.dto.ts
export class QueryUsersDto {
@IsOptional()
@Type(() => Number)
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
search?: string;
@IsOptional()
@IsIn(['createdAt', 'name', 'email'])
sortBy?: string = 'createdAt';
@IsOptional()
@IsIn(['asc', 'desc'])
order?: 'asc' | 'desc' = 'desc';
}
Response DTOs
// user.response.dto.ts
export class UserResponseDto {
id: string;
email: string;
name: string;
avatar?: string;
role: string;
createdAt: Date;
updatedAt: Date;
// Hide sensitive data
static fromEntity(user: User): UserResponseDto {
const { password, ...data } = user;
return data;
}
}
// paginated.response.dto.ts
export class PaginatedResponseDto<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
Controller Implementation
// user.controller.ts
@Controller('users')
@ApiTags('Users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
@ApiOperation({ summary: 'List users' })
@ApiQuery({ type: QueryUsersDto })
@ApiResponse({ status: 200, type: PaginatedResponseDto })
async list(@Query() query: QueryUsersDto): Promise<ResponseDto> {
const { data, total } = await this.userService.findAll(query);
return {
success: true,
data: data.map(UserResponseDto.fromEntity),
pagination: {
page: query.page,
limit: query.limit,
total,
totalPages: Math.ceil(total / query.limit)
}
};
}
@Get(':id')
@ApiOperation({ summary: 'Get user by ID' })
@ApiParam({ name: 'id', type: 'string' })
@ApiResponse({ status: 200, type: UserResponseDto })
@ApiResponse({ status: 404, description: 'User not found' })
async getById(@Param('id') id: string): Promise<ResponseDto> {
const user = await this.userService.findById(id);
if (!user) {
throw new HttpException(
{
success: false,
error: {
code: 'USER_NOT_FOUND',
message: `User with ID ${id} not found`
}
},
HttpStatus.NOT_FOUND
);
}
return {
success: true,
data: UserResponseDto.fromEntity(user)
};
}
@Post()
@ApiOperation({ summary: 'Create user' })
@ApiBody({ type: CreateUserDto })
@ApiResponse({ status: 201, type: UserResponseDto })
async create(@Body() dto: CreateUserDto): Promise<ResponseDto> {
const user = await this.userService.create(dto);
return {
success: true,
data: UserResponseDto.fromEntity(user)
};
}
@Put(':id')
@ApiOperation({ summary: 'Update user' })
@UseGuards(AuthGuard)
async update(
@Param('id') id: string,
@Body() dto: UpdateUserDto
): Promise<ResponseDto> {
const user = await this.userService.update(id, dto);
return {
success: true,
data: UserResponseDto.fromEntity(user)
};
}
@Delete(':id')
@ApiOperation({ summary: 'Delete user' })
@UseGuards(AuthGuard, RolesGuard)
@Roles('admin')
async delete(@Param('id') id: string): Promise<ResponseDto> {
await this.userService.delete(id);
return {
success: true,
data: { deleted: true }
};
}
}
OpenAPI/Swagger Documentation
# openapi/user-service.yaml
openapi: 3.0.0
info:
title: User Service API
version: 1.0.0
description: User management endpoints
servers:
- url: https://api.goodgo.com/v1
paths:
/users:
get:
summary: List users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 10
responses:
'200':
description: List of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
post:
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
'400':
description: Validation error
Pagination Pattern
// pagination.service.ts
export class PaginationService {
paginate<T>(
query: any,
options: {
page: number;
limit: number;
sortBy?: string;
order?: 'asc' | 'desc';
}
) {
const skip = (options.page - 1) * options.limit;
return {
skip,
take: options.limit,
orderBy: options.sortBy ? {
[options.sortBy]: options.order || 'desc'
} : undefined
};
}
}
Error Handling
// error.middleware.ts
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
const isDev = process.env.NODE_ENV === 'development';
// Known errors
if (err instanceof ValidationError) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: err.message,
details: err.errors
}
});
}
if (err instanceof UnauthorizedError) {
return res.status(401).json({
success: false,
error: {
code: 'UNAUTHORIZED',
message: 'Authentication required'
}
});
}
// Unknown errors
logger.error('Unhandled error:', err);
res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: isDev ? err.message : 'Internal server error',
stack: isDev ? err.stack : undefined
}
});
}
Best Practices
-
Resource Naming
- Use plural nouns (
/usersnot/user) - Use kebab-case for multi-word resources
- Keep URLs as short as possible
- Use plural nouns (
-
Versioning
- Include version in URL (
/v1/users) - Maintain backward compatibility
- Deprecate old versions gracefully
- Include version in URL (
-
Security
- Always use HTTPS
- Implement rate limiting
- Validate all inputs
- Use proper authentication/authorization
-
Performance
- Implement pagination for lists
- Use field filtering when possible
- Cache responses appropriately
- Compress responses (gzip)
-
Documentation
- Keep OpenAPI spec up to date
- Include examples in documentation
- Document error responses
- Version your documentation