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