api-design
1
总安装量
1
周安装量
#51048
全站排名
安装命令
npx skills add https://github.com/cacr92/wereply --skill api-design
Skill 文档
API Design Skill
Expert guidance for designing robust, type-safe APIs and data structures in Tauri applications.
Overview
This skill provides guidance for:
- Designing Tauri command APIs
- Creating type-safe DTOs (Data Transfer Objects)
- API documentation and contracts
- Request/response patterns
- Error handling in APIs
- API versioning and evolution
When This Skill Applies
This skill activates when:
- Creating new Tauri commands
- Designing request/response DTOs
- Planning API structure
- Documenting API contracts
- Refactoring existing APIs
- Handling API versioning
API Design Principles
1. Type Safety First
â Good: Explicit, typed structures
use serde::{Deserialize, Serialize};
use specta::Type;
#[derive(Serialize, Deserialize, Type, Clone)]
#[specta(inline)]
pub struct CreateFormulaDto {
/// Formula name (2-100 characters)
#[serde(alias = "formulaName")]
pub name: String,
/// Species code (e.g., "pig", "chicken")
#[serde(alias = "speciesCode")]
pub species_code: String,
/// Optional description
pub description: Option<String>,
/// Formula materials
pub materials: Vec<FormulaMaterialDto>,
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[specta(inline)]
pub struct FormulaMaterialDto {
/// Material code
#[serde(alias = "materialCode")]
pub material_code: String,
/// Proportion in percentage (0-100)
pub proportion: f64,
}
â Bad: Untyped or generic structures
// â Avoid generic maps
pub type RequestData = HashMap<String, serde_json::Value>;
// â Avoid raw JSON
pub fn create_formula(data: serde_json::Value) -> Result<Formula>;
2. Consistent Naming Conventions
Rust â TypeScript Mapping:
| Rust | TypeScript | Notes |
|---|---|---|
snake_case |
camelCase |
Auto-converted by specta |
formula_id |
formulaId |
Use #[serde(alias)] for compatibility |
species_code |
speciesCode |
Keep consistent |
#[derive(Serialize, Deserialize, Type)]
pub struct ApiResponse {
pub formula_id: i64, // TypeScript: formulaId
pub species_code: String, // TypeScript: speciesCode
pub total_cost: f64, // TypeScript: totalCost
}
3. Validation at API Boundaries
use validators::Validators;
#[derive(Serialize, Deserialize, Type, Clone)]
#[specta(inline)]
pub struct CreateFormulaDto {
pub name: String,
pub species_code: String,
pub materials: Vec<FormulaMaterialDto>,
}
impl CreateFormulaDto {
pub fn validate(&self) -> Result<(), ValidationError> {
// Validate name
if self.name.is_empty() {
return Err(ValidationError {
field: "name".to_string(),
message: "åç§°ä¸è½ä¸ºç©º".to_string(),
});
}
if self.name.len() > 100 {
return Err(ValidationError {
field: "name".to_string(),
message: "åç§°ä¸è½è¶
è¿100个å符".to_string(),
});
}
// Validate materials
if self.materials.is_empty() {
return Err(ValidationError {
field: "materials".to_string(),
message: "è³å°éè¦ä¸ç§åæ".to_string(),
});
}
let total_proportion: f64 = self.materials
.iter()
.map(|m| m.proportion)
.sum();
if (total_proportion - 100.0).abs() > 0.01 {
return Err(ValidationError {
field: "materials".to_string(),
message: format!("åææ»æ¯ä¾å¿
须为100%ï¼å½å为{}", total_proportion),
});
}
Ok(())
}
}
#[tauri::command]
#[specta::specta]
pub async fn create_formula(
dto: CreateFormulaDto,
state: State<'_, TauriAppState>,
) -> ApiResponse<Formula> {
// Validate before processing
if let Err(e) = dto.validate() {
return api_err(format!("åæ°éªè¯å¤±è´¥: {}", e.message));
}
// Proceed with creation
with_service(state, |ctx| async move {
ctx.formula_service.create_validated(dto).await
})
.await
}
Request/Response Patterns
Standard Response Format
use serde::{Deserialize, Serialize};
use specta::Type;
/// Standard API response wrapper
#[derive(Serialize, Deserialize, Type)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub message: Option<String>,
pub code: Option<String>,
}
impl<T> ApiResponse<T> {
pub fn ok(data: T) -> Self {
Self {
success: true,
data: Some(data),
message: None,
code: None,
}
}
pub fn err(message: String) -> Self {
Self {
success: false,
data: None,
message: Some(message),
code: Some("ERROR".to_string()),
}
}
pub fn with_code(mut self, code: &str) -> Self {
self.code = Some(code.to_string());
self
}
}
Paginated Response
#[derive(Serialize, Deserialize, Type)]
#[specta(inline)]
pub struct PaginatedResponse<T> {
pub data: Vec<T>,
pub total: usize,
pub page: usize,
pub page_size: usize,
pub total_pages: usize,
}
#[derive(Serialize, Deserialize, Type)]
#[specta(inline)]
pub struct PaginationParams {
pub page: Option<usize>,
pub page_size: Option<usize>,
pub sort_by: Option<String>,
pub sort_order: Option<SortOrder>,
}
#[tauri::command]
#[specta::specta]
pub async fn list_materials(
params: PaginationParams,
state: State<'_, TauriAppState>,
) -> ApiResponse<PaginatedResponse<Material>> {
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
with_service(state, |ctx| async move {
let result = ctx.material_service
.paginate(page, page_size)
.await?;
api_ok(result)
})
.await
}
Error Handling Patterns
Structured Error Types
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize, Type)]
pub enum ApiError {
#[error("Validation failed: {field} - {message}")]
Validation { field: String, message: String },
#[error("Resource not found: {resource} with id {id}")]
NotFound { resource: String, id: i64 },
#[error("Permission denied: {action}")]
Permission { action: String },
#[error("Conflict: {resource} already exists")]
Conflict { resource: String },
#[error("Internal server error: {0}")]
Internal(String),
}
impl From<ApiError> for ApiResponse<()> {
fn from(err: ApiError) -> Self {
let (code, message) = match &err {
ApiError::Validation { .. } => ("VALIDATION_ERROR", err.to_string()),
ApiError::NotFound { .. } => ("NOT_FOUND", err.to_string()),
ApiError::Permission { .. } => ("PERMISSION_DENIED", err.to_string()),
ApiError::Conflict { .. } => ("CONFLICT", err.to_string()),
ApiError::Internal(_) => ("INTERNAL_ERROR", "æå¡å¨å
é¨é误".to_string()),
};
ApiResponse {
success: false,
data: None,
message: Some(message),
code: Some(code.to_string()),
}
}
}
API Documentation
Inline Documentation
/// Creates a new formula with the specified materials.
///
/// # Arguments
///
/// * `dto` - Formula creation data
/// - `name`: Formula name (2-100 characters)
/// - `species_code`: Target species code
/// - `materials`: List of materials with proportions
///
/// # Returns
///
/// Created formula with assigned ID
///
/// # Errors
///
/// - `VALIDATION_ERROR`: Invalid input data
/// - `CONFLICT`: Formula name already exists
///
/// # Example
///
/// ```typescript
/// const result = await commands.createFormula({
/// name: "Growing Pig Formula",
/// speciesCode: "pig",
/// materials: [
/// { materialCode: "corn", proportion: 50.0 },
/// { materialCode: "soybean", proportion: 50.0 }
/// ]
/// });
/// ```
#[tauri::command]
#[specta::specta]
pub async fn create_formula(
dto: CreateFormulaDto,
state: State<'_, TauriAppState>,
) -> ApiResponse<Formula> {
// Implementation
}
TypeScript Usage Documentation
Create corresponding TypeScript documentation:
/**
* Creates a new formula
*
* @param dto - Formula creation data
* @param dto.name - Formula name (2-100 characters)
* @param dto.speciesCode - Target species code (e.g., "pig", "chicken")
* @param dto.materials - Array of materials with proportions
* @returns Promise with created formula
* @throws ValidationError if input data is invalid
* @throws ConflictError if formula name already exists
*
* @example
* ```typescript
* const result = await commands.createFormula({
* name: "Growing Pig Formula",
* speciesCode: "pig",
* materials: [
* { materialCode: "corn", proportion: 50.0 },
* { materialCode: "soybean", proportion: 50.0 }
* ]
* });
*
* if (!result.success) {
* message.error(result.message);
* return;
* }
*
* const formula = result.data;
* console.log(`Created formula with ID: ${formula.id}`);
* ```
*/
API Versioning
Versioning Strategy
// Current version (v1)
#[tauri::command]
#[specta::specta)]
pub async fn create_formula_v1(dto: CreateFormulaDtoV1) -> ApiResponse<Formula> {
// Implementation
}
// New version with additional fields
#[derive(Serialize, Deserialize, Type, Clone)]
#[specta(inline)]
pub struct CreateFormulaDtoV2 {
// Inherit v1 fields
#[serde(flatten)]
pub v1_fields: CreateFormulaDtoV1,
// New fields
pub formula_type: FormulaType,
pub tags: Vec<String>,
}
#[tauri::command]
#[specta::specta]
pub async fn create_formula(dto: CreateFormulaDtoV2) -> ApiResponse<Formula> {
// Use latest version as default
}
Best Practices Checklist
DTO Design â
- All fields have explicit types
- Validation rules are documented
- Field names are consistent with TypeScript conventions
- Optional fields use
Option<T> - Collections have appropriate bounds
- Documentation includes examples
API Contract â
- Request DTOs are validated
- Response format is consistent
- Error codes are standardized
- Error messages are user-friendly
- Success/error responses are clear
- API is documented with examples
Type Safety â
-
#[specta::specta]on all commands -
#[specta(inline)]on all DTOs - Types are generated in
bindings.ts - Frontend uses generated types
- No
as anytype assertions - Error handling is type-safe
Quick Reference
Command Template
#[tauri::command]
#[specta::specta]
pub async fn command_name(
dto: RequestDto,
state: State<'_, TauriAppState>,
) -> ApiResponse<ResponseData> {
// 1. Validate input
dto.validate()?;
// 2. Process with service
with_service(state, |ctx| async move {
ctx.service.do_work(dto).await
})
.await
}
DTO Template
#[derive(Serialize, Deserialize, Type, Clone)]
#[specta(inline)]
pub struct RequestDto {
#[serde(alias = "fieldName")]
pub field_name: String,
pub optional_field: Option<String>,
}
Response Template
const result = await commands.commandName({ fieldName: "value" });
if (!result.success) {
message.error(result.message);
return;
}
const data = result.data;
Common API Patterns
CRUD Operations
// Create
#[tauri::command]
#[specta::specta]
pub async fn create_formula(dto: CreateFormulaDto, state: State<'_>)
-> ApiResponse<Formula>;
// Read
#[tauri::command]
#[specta::specta]
pub async fn get_formula(id: i64, state: State<'_>)
-> ApiResponse<Formula>;
// Update
#[tauri::command]
#[specta::specta]
pub async fn update_formula(id: i64, dto: UpdateFormulaDto, state: State<'_>)
-> ApiResponse<Formula>;
// Delete
#[tauri::command]
#[specta::specta]
pub async fn delete_formula(id: i64, state: State<'_>)
-> ApiResponse<()>;
// List
#[tauri::command]
#[specta::specta]
pub async fn list_formulas(params: ListParams, state: State<'_>)
-> ApiResponse<PaginatedResponse<Formula>>;
When to Use This Skill
Activate this skill when:
- Designing new Tauri commands
- Creating request/response DTOs
- Planning API structure
- Documenting APIs
- Validating API design
- Handling API errors
- Versioning APIs