- 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.
485 lines
10 KiB
Markdown
485 lines
10 KiB
Markdown
---
|
|
name: api-design
|
|
description: 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
|
|
|
|
1. **Consistency**: All APIs follow the same patterns
|
|
2. **Predictability**: Developers can guess endpoint behavior
|
|
3. **Simplicity**: Easy to understand and use
|
|
4. **Documentation**: Self-documenting through OpenAPI
|
|
5. **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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
1. **Resource Naming**
|
|
- Use plural nouns (`/users` not `/user`)
|
|
- Use kebab-case for multi-word resources
|
|
- Keep URLs as short as possible
|
|
|
|
2. **Versioning**
|
|
- Include version in URL (`/v1/users`)
|
|
- Maintain backward compatibility
|
|
- Deprecate old versions gracefully
|
|
|
|
3. **Security**
|
|
- Always use HTTPS
|
|
- Implement rate limiting
|
|
- Validate all inputs
|
|
- Use proper authentication/authorization
|
|
|
|
4. **Performance**
|
|
- Implement pagination for lists
|
|
- Use field filtering when possible
|
|
- Cache responses appropriately
|
|
- Compress responses (gzip)
|
|
|
|
5. **Documentation**
|
|
- Keep OpenAPI spec up to date
|
|
- Include examples in documentation
|
|
- Document error responses
|
|
- Version your documentation |