This document outlines the coding standards, patterns, and best practices for the Mundam backend, built with Rust and Tauri.
- Thin Commands: Tauri commands (
#[tauri::command]) should be thin wrappers. They should validate input, call business logic functions, and handle errors. They should rarely contain complex business logic themselves. - Domain Modules: Commands must be organized into semantic domain modules. Avoid putting commands in the root
src-tauri/src/.library/commands/: Core assets management (tags, folders, metadata).media/commands.rs: Audio/Video processing and metadata extraction commands.thumbnails/commands.rs: Thumbnail generation and priority management.settings/commands.rs: Configuration and maintenance.transcoding/commands.rs: Streaming and transcoding logic.
- State Management: Use
app.manage()to inject state. Access state in commands usingtauri::State<T>.
Whenever a new command is created, you MUST update the Tauri permissions:
src-tauri/permissions/main.toml: Define a new permission for the command.src-tauri/capabilities/default.json: Add the new permission to the default visibility list (or appropriate capability set).
// ✅ Correct Example: Organzed Command
// Localized in src/library/commands/tags.rs
#[tauri::command]
pub async fn create_tag(
db: State<'_, Arc<Db>>,
name: String,
) -> AppResult<i64> {
Ok(db.create_tag(&name).await?)
}- Never abbreviate variable names. Each variable name must describe exactly its responsibility.
// ✅ Correct let processed_image_buffer = ...; // ❌ Avoid let buf = ...;
- Variables & Functions: snake_case (e.g.,
process_image,user_id) - Types & Traits: PascalCase (e.g.,
ThumbnailStrategy,AppState) - Constants: SCREAMING_SNAKE_CASE (e.g.,
MAX_RETRY_ATTEMPTS)
-
Single Responsibility Principle (SRP): Each function or module must have only one clear purpose.
-
Readability over cleverness: Favor code that is easy to understand over complex iterator chains or macros unless necessary for performance.
- Avoid deeply nested
matchstatements: Break them into functions.
- Avoid deeply nested
-
Explicit Error Handling: Always handle errors. Never swallow them without logging or context.
We use a centralized error management system to ensure consistency, tipability, and clear communication with the frontend.
- Centralized Enum (
AppError): All errors are defined insrc/error.rsusing thethiserrorcrate. This allows for automatic conversion from external errors (SQLx, IO, Tauri) using#[from]. - Primary Result Type (
AppResult<T>): Almost all functions and commands should returnAppResult<T>, which is an alias forResult<T, AppError>. - No
unwrap(): Avoid.unwrap()or.expect()in production code (except in tests or unavoidable initialization). Use the?operator for clean propagation. - Frontend Serialization:
AppErrorimplementsserde::Serializeto return structured information to the JavaScript/TypeScript frontend instead of raw strings.
// ✅ Correct Example: Using AppResult in a command
#[tauri::command]
pub async fn get_data(db: State<'_, Arc<Db>>) -> AppResult<Vec<Data>> {
// Automatic conversion from sqlx::Error to AppError via ?
let result = db.fetch_all().await?;
Ok(result)
}
// ✅ Correct Example: Handling specific failures
if !path.exists() {
return Err(AppError::NotFound(format!("Path missing: {}", path)));
}- Logging: Errors should be logged at the point of origin or in the command handler if they indicate critical failures.
We use the tracing ecosystem instead of raw println! macros. This ensures structured logging, trace correlation, and potential OpenTelemetry (OTLP) integration in development.
- Use
tracingmacros (info!,debug!,warn!,error!): Never useprintln!in library code or commands unless it's a CLI-specific stdout output. - Instrument Commands: Tauri commands and heavy I/O operations should be annotated with
#[tracing::instrument]to automatically track span lifecycles. - OTLP Feature: In development, we can enable OpenTelemetry exporters to visualize bottlenecks (like heavy SQLite transactions or File System delays) via Jaeger/Prometheus without bloating the production binary.
// ✅ Correct Example: Using tracing instrumentation
use tracing::{info, instrument, error};
#[tauri::command]
#[instrument(skip(db), err)]
pub async fn ingest_assets(db: State<'_, Arc<Db>>, path: String) -> AppResult<usize> {
info!("Starting ingestion for path: {}", path);
match db.ingest(&path).await {
Ok(count) => {
info!("Successfully ingested {} items", count);
Ok(count)
},
Err(e) => {
error!("Failed to ingest: {:?}", e);
Err(e.into())
}
}
}- Use
.awaitresponsibly. Avoid blocking the async runtime (Tokio) with heavy CPU-bound tasks. Usetokio::task::spawn_blockingfor heavy synchronous operations like image processing or file I/O.
// ✅ Correct handling of heavy work
pub async fn heavy_processing() -> Result<(), String> {
let result = tokio::task::spawn_blocking(|| {
// CPU intensive work here
compute_hash()
}).await.map_err(|e| e.to_string())?;
Ok(result)
}We adhere to Clippy lints. Warnings should be treated as errors. Run strict checks locally:
cargo clippy -- -D warnings- Follow standard
rustfmtrules. - Run
cargo fmtbefore every commit.
Every item, including functions, methods, structs, enums, traits, and their respective fields (properties), whether public or private, MUST have documentation via ///. The goal is for any developer to understand the intent and risks without having to read the implementation.
- Summary: A short, direct line describing the purpose.
# ErrorsSection: Mandatory if the function returnsResult. Must list failure conditions.# PanicsSection: Mandatory if the function containsunwrap(),expect(), or could logically panic.# ExamplesSection: Highly recommended for complex modules or global utilities.
/// Processes image resizing while maintaining aspect ratio.
///
/// # Arguments
/// * `buffer` - Byte vector of the original image.
/// * `dimensions` - Desired tuple (width, height).
///
/// # Errors
/// Returns `Err` if the image format is unsupported or if the buffer is corrupted.
///
/// # Examples
/// ```rust
/// let resized = processor::resize(my_bytes, (300, 300)).await?;
/// ```
pub async fn resize(buffer: Vec<u8>, dimensions: (u32, u32)) -> Result<Vec<u8>, Error> { ... }Use //! at the top of files to describe the module's responsibility and how it integrates into the system. This provides the "Big Picture" necessary before diving into specific functions.
Comments within functions must follow the "Why, not What" rule. Code should be self-explanatory (What); comments should explain business logic or technical constraints (Why).
- What to COMMENT:
- Non-obvious technical decisions (e.g., "We use 600ms debounce because the macOS file system takes time to release the lock").
- Complex algorithms or mathematical formulas.
- Security or performance notes.
TODOorFIXMEwith a clear description.
- What NOT to COMMENT (Pollution):
- The obvious:
let count = 10; // sets count to 10. - Commented-out code: If it's not useful, delete it. History is in Git.
- Variable descriptions: Use descriptive names instead of comments next to them.
- The obvious:
// ❌ POLLUTION: Redundant comment
let images = db.get_images().await?; // fetches images from database
// ✅ CORRECT: Explains technical motivation
// We start a manual transaction here to avoid multiple disk flushes,
// which is critical for performance on traditional HDDs.
let mut transaction = pool.begin().await?;The application relies on a centralized registry of supported file formats to consistently handle detection, thumbnail generation, and playback strategies.
- Definitions:
src-tauri/src/formats/definitions.rs - Types:
src-tauri/src/formats/types.rs - Logic:
src-tauri/src/formats/mod.rs
Use the crate::formats::FileFormat struct for all format-related logic.
use crate::formats::FileFormat;
use crate::formats::MediaType;
// 1. Detect format (File signature > Extension)
if let Some(format) = FileFormat::detect(path) {
match format.type_category {
MediaType::Image => { ... },
MediaType::Video => { ... },
_ => { ... }
}
}
// 2. Quick check (Indexer / File Walker)
if FileFormat::is_supported_extension(path) {
// Process file...
}- Open
src-tauri/src/formats/definitions.rs. - Add a new
FileFormatentry to theSUPPORTED_FORMATSarray. - Specify the properties:
name: Human-readable name (e.g., "WebP Image").extensions: List of lowercase extensions (e.g.,&["webp"]).mime_types: Standard MIME types.type_category:MediaType::Image,Video,Audio,Project, etc.strategy: How thumbnails are generated (NativeImage,Ffmpeg,NativeExtractor, etc.).playback: How it is played in the UI (Native,Hls,LinearHls,None).
FileFormat {
name: "New Format",
extensions: &["new", "nf"],
mime_types: &["image/x-new"],
type_category: MediaType::Image,
strategy: ThumbnailStrategy::NativeImage,
playback: PlaybackStrategy::None,
},To retrieve the list of supported formats in the frontend, invoke the get_library_supported_formats command:
import { invoke } from '@tauri-apps/api/core';
interface FileFormat {
name: string;
extensions: string[];
mimeTypes: string[];
typeCategory:
| 'Image'
| 'Video'
| 'Audio'
| 'Project'
| 'Archive'
| 'Model3D'
| 'Font'
| 'Unknown';
playback:
| 'Native'
| 'Hls'
| 'LinearHls'
| 'AudioHls'
| 'AudioLinearHls'
| 'Transcode'
| 'AudioTranscode'
| 'None';
}
const formats = await invoke<FileFormat[]>('get_library_supported_formats');The database layer is organized into the src/db/ module. NEVER place database logic in commands or multiple flat files. Follow the domain-driven modular structure:
db/mod.rs: Entry point,Dbstruct, initialization, and maintenance.db/models.rs: Single source of truth for all database-related structs (DTOs). No duplication across the system.db/images.rs: Image/File persistence.db/folders.rs: Hierarchy and location management.db/tags.rs: Taxonomy and relationships.db/search.rs: Dynamic query building withQueryBuilder.
- Use
sqlx::query!andsqlx::query_as!: Favor macros for compile-time validation. - Non-Null Indicators: SQLite nullability detection can fail. Use the "force non-null" syntax for columns you know are
NOT NULL:SELECT id AS "id!", name FROM tags
- Type Overrides: For custom types (like
chrono::DateTime), use explicit type hints in the query if detection fails:SELECT created_at AS "created_at: DateTime<Utc>" FROM images
- Case Consistency: Backend models should avoid
#[serde(rename_all = "camelCase")]unless strictly required, to maintain consistency with the SQL schema and existing frontend property expectations (which often usesnake_casefrom the API).
Because we use sqlx macros (query!, query_as!), the Rust compiler requires an active database connection to verify queries during type-checking. To ensure our CI pipelines (GitHub Actions) succeed without spinning up a live database:
- Always run
cargo sqlx prepareinside thesrc-tauri/directory whenever you add, modify, or delete any SQL query. - Commit the
.sqlx/folder: This command generates/updates thesrc-tauri/.sqlx/directory, which acts as an offline cache. This folder must be committed. - The CI workflow uses
SQLX_OFFLINE=trueto consume this cache instead of a live connection.
- Batch Operations: Use dedicated
batchfunctions for high-volume operations (indexing). - Transactions: Wrap multi-step logic in
pool.begin(). Reusable helpers should accept&mut SqliteConnectionor&mut SqliteTransaction. - Wal Mode: Optimized for concurrent reads and single writer.
// ✅ Implementation pattern in db/domain.rs
impl Db {
pub async fn add_item(&self, name: &str) -> Result<i64, sqlx::Error> {
let res = sqlx::query!("INSERT INTO items (name) VALUES (?)", name)
.execute(&self.pool)
.await?;
Ok(res.last_insert_rowid())
}
}