| name | backend-dev-guidelines |
| description | Comprehensive Rust backend development guide for Tauri applications. Use when creating Tauri commands, database operations, error handling, state management, or async patterns. Covers layered architecture (commands → services → operations → database), Result-based error handling, SeaORM patterns, tokio async/await, testing with cargo test, and type-driven design principles. Use when this capability is needed. |
| metadata | {"author":"emilyantosch"} |
Backend Development Guidelines
Purpose
Establish consistency and best practices for Rust backend development in Tauri applications using type-driven design, robust error handling, and async patterns with tokio.
When to Use This Skill
Automatically activates when working on:
- Creating or modifying Tauri commands (backend-to-frontend functions)
- Implementing service layer business logic
- Database operations with SeaORM (Entity, ActiveModel, transactions)
- Error handling with thiserror and anyhow
- State management (Arc, Mutex, RwLock, Tauri State)
- Async operations with tokio (spawn, join!, timeout)
- Backend testing with cargo test and tokio::test, unit tests in file, integration test in tests/
- Prefer property-based testing
- Type-driven design (newtypes, enums, Result/Option)
Quick Start
New Backend Feature Checklist
New Tauri Command Checklist
Architecture Overview
Layered Architecture
Frontend (TypeScript/React)
↓
Tauri IPC (invoke)
↓
Command Handler (#[command])
↓
Controller Layer (hands off command to service layer)
↓
Service Layer (business logic)
↓
Repository (SeaORM)
↓
Database (SQLite via SeaORM)
Key Principle: Each layer has ONE responsibility.
- Commands: Handle Tauri IPC, validate input, delegate to services
- Services: Implement business logic, coordinate operations
- Operations: Execute database queries, manage transactions
- Database: Store and retrieve data
Directory Structure
src-tauri/src/
├── commands/ # Tauri command handlers (#[command])
├── controllers/ # Controller handlers send IPC commands to service business logic
├── services/ # Business logic layer
├── database/ # SeaORM operations and manager
│ ├── repository.rs # Database operation structs
│ └── manager.rs # DatabaseManager, connection pool
├── entities/ # SeaORM entity definitions (generated)
├── errors/ # Error type definitions (thiserror)
│ ├── db.rs # Database errors
│ ├── app.rs # Application errors
│ └── file_system.rs # File system errors
├── config/ # Configuration and state
│ ├── app.rs # AppState struct
│ └── database.rs # Database configuration
├── data/ # Data structures and types
├── tests/ # Integration and unit tests
├── lib.rs # Library root
└── main.rs # Application entry point
Naming Conventions:
- Files:
snake_case - tag_management.rs, watched_folder_management.rs
- Structs:
PascalCase - AppState, DatabaseManager, FileOperations
- Functions:
snake_case - create_tag, get_all_tags, scan_directory
- Modules:
snake_case - mod commands;, mod database;
Core Principles (8 Key Rules)
1. Commands Only Delegate, Controllers delegate, Services Implement Logic
#[command]
pub async fn create_tag(state: State<'_, Mutex<AppState>>, name: String) -> Result<Tag, AppError> {
}
#[command]
pub async fn create_tag(state: State<'_, Mutex<AppState>>, name: String) -> Result<Tag, AppError> {
tag_service::create_tag(&state, name).await
}
2. Use anyhow::Result for All Fallible Operations, Result<T, E> for IPC endpoints
let user = database.get_user(id).unwrap();
pub async fn get_user(id: i32) -> Result<User, DbError> {
let user = Entity::find_by_id(id)
.one(&*connection)
.await?
.ok_or(DbError::NotFound)?;
Ok(user)
}
3. Leverage the Type System (Make Illegal States Unrepresentable)
pub fn create_email(email: String) -> Result<(), Error> { ... }
pub struct Email(String);
impl Email {
pub fn new(email: String) -> Result<Self, ValidationError> {
Ok(Email(email))
}
}
4. All new types should live in app/data
pub fn create_email(email: String) -> Result<(), Error> { ... }
pub struct Email(String);
impl Email {
pub fn new(email: String) -> Result<Self, ValidationError> {
Ok(Email(email))
}
}
- Data is split in Internal (for backend use) and commands (for IPC endpoints)
- For database representation, entites live in app/entity
5. Prefer Arc for Shared Ownership
let db_copy = database_manager.clone();
pub struct AppState {
pub database: Arc<DatabaseManager>,
pub cache: Arc<RwLock<HashMap<String, Value>>>,
}
6. Use thiserror for Domain Errors
#[derive(Debug, Error, Serialize, Deserialize)]
pub enum TagError {
#[error("Tag not found: {id}")]
NotFound { id: i32 },
#[error("Tag already exists: {name}")]
AlreadyExists { name: String },
#[error("Database error: {0}")]
Database(#[from] DbError),
}
7. Instrument with Tracing
use tracing::{info, error, instrument};
#[instrument(skip(state))]
pub async fn scan_directory(state: &AppState, path: PathBuf) -> Result<Vec<File>, ScanError> {
info!(path = ?path, "Starting directory scan");
}
8. Write Tests First (TDD with std::test and tokio::test)
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_tag() -> Result<()> {
let db = setup_test_db().await?;
let tag = create_tag(&db, "test".to_string()).await?;
assert_eq!(tag.name, "test");
Ok(())
}
}
- Prefer
std::test where async is not necessary
Common Imports
use tauri::{command, State};
use sea_orm::{
entity::prelude::*,
ActiveModelTrait, DatabaseConnection, EntityTrait,
QueryFilter, QuerySelect, Set,
};
use thiserror::Error;
use anyhow::{Context, Result};
use tokio;
use serde::{Serialize, Deserialize};
use std::sync::{Arc, Mutex, RwLock};
use tracing::{info, debug, warn, error, instrument};
use std::path::PathBuf;
use std::collections::HashMap;
Quick Reference
Common Result Types
| Pattern | Use Case |
|---|
anyhow::Result<T> | Fallible operations (preferred if applicable) |
Result<T, E> | Fallible operations |
Option<T> | Optional values |
Result<(), E> | Operations with no return value |
anyhow::Result<T> | Quick error handling in applications |
Anti-Patterns to Avoid
❌ Business logic in command handlers (delegate to services)
❌ Using .unwrap() or .expect() in production code
❌ Missing error handling (always return Result<T, E>)
❌ Primitive obsession (use newtypes for domain concepts)
❌ Large lock scopes (keep Mutex/RwLock guards minimal)
❌ println! for logging (use tracing macros instead)
❌ Cloning Arc contents (clone the Arc, not the data)
Navigation Guide
Resource Files
Command handlers with #[command], State injection, Result returns, registration
thiserror error enums, anyhow Context, ? operator, Tauri serialization
Entity and ActiveModel patterns, CRUD operations, transactions, query builders
tokio async/await, spawn, join!/select!, timeout, async traits
Arc/Mutex/RwLock patterns, Tauri State, AppState structure, lock scopes
Cargo test framework, #[tokio::test], assertions, mocking, test organization
Tracing macros, #[instrument], structured logging, subscriber setup
Newtype pattern, ADTs with enums, Option/Result transforms, From/Into traits
Borrowing rules, lifetimes, Arc for shared ownership, Clone strategies
End-to-end examples from Hestia, full CRUD flows, testing examples
Related Skills
- error-tracking - Error tracking and monitoring patterns
- skill-developer - Meta-skill for creating and managing skills
- frontend-dev-guidelines - Frontend development patterns for Tauri apps
Skill Status: Phase 1.1 COMPLETE ✅
Line Count: < 350 ✅
Progressive Disclosure: 10 resource files (4 created, 6 remaining) 🚧
Transformation: TypeScript → Rust COMPLETE ✅
Converted and distributed by TomeVault — claim your Tome and manage your conversions.