- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes. - Introduced configuration loading flow in configuration-management skill to clarify the configuration process. - Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers. - Enhanced various skills with additional diagrams to improve understanding of complex concepts. These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
602 lines
14 KiB
Markdown
602 lines
14 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
|
|
|
|
## Request/Response Flow
|
|
|
|
The following diagram illustrates how a request flows through the API layers:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Client
|
|
participant Middleware
|
|
participant Controller
|
|
participant Service
|
|
participant Repository
|
|
participant Database
|
|
|
|
Client->>Middleware: HTTP Request
|
|
Middleware->>Middleware: Authentication<br/>Rate Limiting<br/>Validation
|
|
Middleware->>Controller: Validated Request
|
|
Controller->>Controller: Parse DTO<br/>Extract Params
|
|
Controller->>Service: Business Logic Call
|
|
Service->>Repository: Data Access Call
|
|
Repository->>Database: Query Execution
|
|
Database-->>Repository: Data Result
|
|
Repository-->>Service: Entity/Entities
|
|
Service-->>Controller: Business Result
|
|
Controller->>Controller: Transform to DTO<br/>Format Response
|
|
Controller-->>Middleware: Response Object
|
|
Middleware-->>Client: HTTP Response
|
|
```
|
|
|
|
## API Structure Hierarchy
|
|
|
|
The following diagram shows the hierarchical structure of RESTful API endpoints:
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[API Base URL<br/>https://api.goodgo.com] --> B[Version<br/>/v1]
|
|
B --> C[Resource Collection<br/>/users]
|
|
B --> D[Resource Collection<br/>/orders]
|
|
B --> E[Resource Collection<br/>/products]
|
|
|
|
C --> F[Resource Instance<br/>/users/:id]
|
|
C --> G[Sub-Resource<br/>/users/:id/orders]
|
|
|
|
F --> H[GET /users/:id<br/>Retrieve user]
|
|
F --> I[PUT /users/:id<br/>Update user]
|
|
F --> J[DELETE /users/:id<br/>Delete user]
|
|
|
|
C --> K[GET /users<br/>List users]
|
|
C --> L[POST /users<br/>Create user]
|
|
|
|
G --> M[GET /users/:id/orders<br/>List user orders]
|
|
G --> N[POST /users/:id/orders<br/>Create order]
|
|
|
|
style A fill:#e1f5ff
|
|
style B fill:#b3e5fc
|
|
style C fill:#81d4fa
|
|
style D fill:#81d4fa
|
|
style E fill:#81d4fa
|
|
style F fill:#4fc3f7
|
|
style G fill:#4fc3f7
|
|
```
|
|
|
|
## 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
|
|
|
|
The following diagram illustrates the error handling flow in the API:
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Request Received] --> B{Validation<br/>Middleware}
|
|
B -->|Invalid| C[ValidationError]
|
|
B -->|Valid| D[Controller]
|
|
|
|
D --> E{Business Logic}
|
|
E -->|Not Found| F[NotFoundError]
|
|
E -->|Unauthorized| G[UnauthorizedError]
|
|
E -->|Forbidden| H[ForbiddenError]
|
|
E -->|Conflict| I[ConflictError]
|
|
E -->|Success| J[Return Success Response]
|
|
|
|
E -->|Unexpected Error| K[Generic Error]
|
|
|
|
C --> L[Error Handler<br/>Middleware]
|
|
F --> L
|
|
G --> L
|
|
H --> L
|
|
I --> L
|
|
K --> L
|
|
|
|
L --> M{Error Type?}
|
|
M -->|ValidationError| N[400 Bad Request<br/>VALIDATION_ERROR]
|
|
M -->|UnauthorizedError| O[401 Unauthorized<br/>UNAUTHORIZED]
|
|
M -->|ForbiddenError| P[403 Forbidden<br/>FORBIDDEN]
|
|
M -->|NotFoundError| Q[404 Not Found<br/>NOT_FOUND]
|
|
M -->|ConflictError| R[409 Conflict<br/>CONFLICT]
|
|
M -->|Unknown Error| S{Development<br/>Mode?}
|
|
|
|
S -->|Yes| T[500 Internal Error<br/>INTERNAL_ERROR<br/>+ Stack Trace]
|
|
S -->|No| U[500 Internal Error<br/>INTERNAL_ERROR<br/>Generic Message]
|
|
|
|
N --> V[Format Error Response<br/>success: false<br/>error: code, message, details]
|
|
O --> V
|
|
P --> V
|
|
Q --> V
|
|
R --> V
|
|
T --> V
|
|
U --> V
|
|
|
|
V --> W[Log Error<br/>Server-Side]
|
|
W --> X[Send HTTP Response]
|
|
J --> X
|
|
|
|
style A fill:#e1f5ff
|
|
style J fill:#c8e6c9
|
|
style L fill:#fff9c4
|
|
style V fill:#ffccbc
|
|
style X fill:#e1f5ff
|
|
```
|
|
|
|
### Error Handler Implementation
|
|
|
|
```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 |