mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
- Introduced a decision tree to help determine when to use PATCH versus dedicated endpoints based on operation characteristics. - Added guidelines for naming dedicated endpoints and provided examples for various scenarios, enhancing clarity on API design practices.
6.4 KiB
6.4 KiB
API Design Guidelines
Guidelines for designing RESTful APIs in the Cherry Studio Data API system.
Path Naming
| Rule | Example | Notes |
|---|---|---|
| Use plural nouns for collections | /topics, /messages |
Resources are collections |
| Use kebab-case for multi-word paths | /user-settings |
Not camelCase or snake_case |
| Express hierarchy via nesting | /topics/:topicId/messages |
Parent-child relationships |
| Avoid verbs for CRUD operations | /topics not /getTopics |
HTTP methods express action |
HTTP Method Semantics
| Method | Purpose | Idempotent | Typical Response |
|---|---|---|---|
| GET | Retrieve resource(s) | Yes | 200 + data |
| POST | Create resource | No | 201 + created entity |
| PUT | Replace entire resource | Yes | 200 + updated entity |
| PATCH | Partial update | Yes | 200 + updated entity |
| DELETE | Remove resource | Yes | 204 / void |
Standard Endpoint Patterns
// Collection operations
'/topics': {
GET: { ... } // List with pagination/filtering
POST: { ... } // Create new resource
}
// Individual resource operations
'/topics/:id': {
GET: { ... } // Get single resource
PUT: { ... } // Replace resource
PATCH: { ... } // Partial update
DELETE: { ... } // Remove resource
}
// Nested resources (use for parent-child relationships)
'/topics/:topicId/messages': {
GET: { ... } // List messages under topic
POST: { ... } // Create message in topic
}
PATCH vs Dedicated Endpoints
Decision Criteria
Use this decision tree to determine the appropriate approach:
Operation characteristics:
├── Simple field update with no side effects?
│ └── Yes → Use PATCH
├── High-frequency operation with clear business meaning?
│ └── Yes → Use dedicated endpoint (noun-based sub-resource)
├── Operation triggers complex side effects or validation?
│ └── Yes → Use dedicated endpoint
├── Operation creates new resources?
│ └── Yes → Use POST to dedicated endpoint
└── Default → Use PATCH
Guidelines
| Scenario | Approach | Example |
|---|---|---|
| Simple field update | PATCH | PATCH /messages/:id { data: {...} } |
| High-frequency + business meaning | Dedicated sub-resource | PUT /topics/:id/active-node { nodeId } |
| Complex validation/side effects | Dedicated endpoint | POST /messages/:id/move { newParentId } |
| Creates new resources | POST dedicated | POST /messages/:id/duplicate |
Naming for Dedicated Endpoints
- Prefer noun-based paths over verb-based when possible
- Treat the operation target as a sub-resource:
/topics/:id/active-nodenot/topics/:id/switch-branch - Use POST for actions that create resources or have non-idempotent side effects
- Use PUT for setting/replacing a sub-resource value
Examples
// ✅ Good: Noun-based sub-resource for high-frequency operation
PUT /topics/:id/active-node
{ nodeId: string }
// ✅ Good: Simple field update via PATCH
PATCH /messages/:id
{ data: MessageData }
// ✅ Good: POST for resource creation
POST /messages/:id/duplicate
{ includeDescendants?: boolean }
// ❌ Avoid: Verb in path when noun works
POST /topics/:id/switch-branch // Use PUT /topics/:id/active-node instead
// ❌ Avoid: Dedicated endpoint for simple updates
POST /messages/:id/update-content // Use PATCH /messages/:id instead
Non-CRUD Operations
Use verb-based paths for operations that don't fit CRUD semantics:
// Search
'/topics/search': {
GET: { query: { q: string } }
}
// Statistics / Aggregations
'/topics/stats': {
GET: { response: { total: number, ... } }
}
// Resource actions (state changes, triggers)
'/topics/:id/archive': {
POST: { response: { archived: boolean } }
}
'/topics/:id/duplicate': {
POST: { response: Topic }
}
Query Parameters
| Purpose | Pattern | Example |
|---|---|---|
| Pagination | page + limit |
?page=1&limit=20 |
| Sorting | orderBy + order |
?orderBy=createdAt&order=desc |
| Filtering | direct field names | ?status=active&type=chat |
| Search | q or search |
?q=keyword |
Response Status Codes
Use standard HTTP status codes consistently:
| Status | Usage | Example |
|---|---|---|
| 200 OK | Successful GET/PUT/PATCH | Return updated resource |
| 201 Created | Successful POST | Return created resource |
| 204 No Content | Successful DELETE | No body |
| 400 Bad Request | Invalid request format | Malformed JSON |
| 401 Unauthorized | Authentication required | Missing/invalid token |
| 403 Forbidden | Permission denied | Insufficient access |
| 404 Not Found | Resource not found | Invalid ID |
| 409 Conflict | Concurrent modification | Version conflict |
| 422 Unprocessable | Validation failed | Invalid field values |
| 429 Too Many Requests | Rate limit exceeded | Throttling |
| 500 Internal Error | Server error | Unexpected failure |
Error Response Format
All error responses follow the DataApiError structure:
interface DataApiError {
code: string // ErrorCode enum value (e.g., 'NOT_FOUND')
message: string // Human-readable error message
status: number // HTTP status code
details?: any // Additional context (e.g., field errors)
stack?: string // Stack trace (development only)
}
Examples:
// 404 Not Found
{
code: 'NOT_FOUND',
message: "Topic with id 'abc123' not found",
status: 404,
details: { resource: 'Topic', id: 'abc123' }
}
// 422 Validation Error
{
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
status: 422,
details: {
fieldErrors: {
name: ['Name is required', 'Name must be at least 3 characters'],
email: ['Invalid email format']
}
}
}
Use DataApiErrorFactory utilities to create consistent errors:
import { DataApiErrorFactory } from '@shared/data/api'
throw DataApiErrorFactory.notFound('Topic', id)
throw DataApiErrorFactory.validation({ name: ['Required'] })
throw DataApiErrorFactory.database(error, 'insert topic')
Naming Conventions Summary
| Element | Case | Example |
|---|---|---|
| Paths | kebab-case, plural | /user-settings, /topics |
| Path params | camelCase | :topicId, :messageId |
| Query params | camelCase | orderBy, pageSize |
| Body fields | camelCase | createdAt, userName |
| Error codes | SCREAMING_SNAKE | NOT_FOUND, VALIDATION_ERROR |