Files
pos-system/docs/en/skills/api-design.md
Ho Ngoc Hai 9b6c585f57 Enhance documentation structure and improve bilingual support across skills
- 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.
2026-01-01 07:35:44 +07:00

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

  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

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

  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