api-design

Expert guidance for designing robust, type-safe APIs and data structures in Tauri applications.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "api-design" with this command: npx skills add cacr92/wereply/cacr92-wereply-api-design

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>;

  1. 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 }

  1. 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: &#x26;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
  • 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&#x3C;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 any type 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

pdf

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

error-handling-debugging

No summary provided by upstream source.

Repository SourceNeeds Review
General

deepseek-integration

No summary provided by upstream source.

Repository SourceNeeds Review