| name | api-design |
| description | 当用户要求"设计API"、"创建DTO"、"定义数据结构"、"API文档"、"请求响应格式",或者提到"API设计"、"数据传输对象"、"契约"、"接口"时使用此技能。用于 API 设计模式、数据建模、类型安全或后端 API 结构设计。 |
| version | 2.0.0 |
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 {
#[serde(alias = "formulaName")]
pub name: String,
#[serde(alias = "speciesCode")]
pub species_code: String,
pub description: Option<String>,
pub materials: Vec<FormulaMaterialDto>,
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[specta(inline)]
pub struct FormulaMaterialDto {
#[serde(alias = "materialCode")]
pub material_code: String,
pub proportion: f64,
}
✗ Bad: Untyped or generic structures
pub type RequestData = HashMap<String, serde_json::Value>;
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,
pub species_code: String,
pub total_cost: f64,
}
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> {
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(),
});
}
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> {
if let Err(e) = dto.validate() {
return api_err(format!("参数验证失败: {}", e.message));
}
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;
#[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
#[tauri::command]
#[specta::specta]
pub async fn create_formula(
dto: CreateFormulaDto,
state: State<'_, TauriAppState>,
) -> ApiResponse<Formula> {
}
TypeScript Usage Documentation
Create corresponding TypeScript documentation:
API Versioning
Versioning Strategy
#[tauri::command]
#[specta::specta)]
pub async fn create_formula_v1(dto: CreateFormulaDtoV1) -> ApiResponse<Formula> {
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[specta(inline)]
pub struct CreateFormulaDtoV2 {
#[serde(flatten)]
pub v1_fields: CreateFormulaDtoV1,
pub formula_type: FormulaType,
pub tags: Vec<String>,
}
#[tauri::command]
#[specta::specta]
pub async fn create_formula(dto: CreateFormulaDtoV2) -> ApiResponse<Formula> {
}
Best Practices Checklist
DTO Design ✅
API Contract ✅
Type Safety ✅
Quick Reference
Command Template
#[tauri::command]
#[specta::specta]
pub async fn command_name(
dto: RequestDto,
state: State<'_, TauriAppState>,
) -> ApiResponse<ResponseData> {
dto.validate()?;
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
#[tauri::command]
#[specta::specta]
pub async fn create_formula(dto: CreateFormulaDto, state: State<'_>)
-> ApiResponse<Formula>;
#[tauri::command]
#[specta::specta]
pub async fn get_formula(id: i64, state: State<'_>)
-> ApiResponse<Formula>;
#[tauri::command]
#[specta::specta]
pub async fn update_formula(id: i64, dto: UpdateFormulaDto, state: State<'_>)
-> ApiResponse<Formula>;
#[tauri::command]
#[specta::specta]
pub async fn delete_formula(id: i64, state: State<'_>)
-> ApiResponse<()>;
#[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