Files
pos-system/docs/en/skills/api-design.md
Ho Ngoc Hai 2640b351c3 Enhance documentation with detailed diagrams and structured flows
- 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.
2026-01-01 23:22:54 +07:00

14 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

  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:

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:

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

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

The following diagram illustrates the error handling flow in the API:

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

// 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