--- 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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( 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