My Rust Backend Journey: A Practical Guide from 0 to 1


📦 GitHub:https://github.com/idaibin/rustzen-admin

I. What is Rustzen? Why this naming?

Rustzen is the name I chose for this project, meaning “Rust + Zen”: Rust brings performance and safety, Zen represents minimalism and order.

This is not just a combination of technology stacks, but my way of thinking about development experience:

Minimalism is not “doing less”, but “just right”. Every evolution of Rustzen is a question about “appropriate design + clear boundaries”.

I started from Tauri + SQLite, gradually moved to Axum + SQLx, and continuously refined the current structure through evolution. The starting point of this structure is a response to the following problems:

The emergence of Rustzen is a response to these problems—not pursuing complex frameworks, but seeking “just right” in structure.


II. Why adopt a three-layer architecture?

Why design it this way?

Clear structure is the foundation of maintainability. Three-layer separation allows each layer to focus only on its own responsibilities, reducing cognitive load.

LayerResponsibility DescriptionCore Features
routerRoute definition, permission binding, HTTP processing, intuitive functional entryRouter + Handler merged
serviceAggregate business logic, focus on behavior, encapsulate processing flowPure business logic, no framework dependency
repoDatabase access, encapsulate SQL queriesFirst-line error handling

Directory Example:

features/
└── system/
    ├── mod.rs           // Module declaration
    ├── user/
    │   ├── mod.rs
    │   ├── router.rs    // Routes + handlers
    │   ├── service.rs   // Business logic
    │   ├── repo.rs      // Data access
    │   ├── entity.rs    // Database entities
    │   ├── dto.rs       // Request data transfer objects
    │   └── vo.rs        // Response view objects
    ├── role/
    └── menu/

III. Why split data models into entity/dto/vo?

Why design it this way?

entity/dto/vo separation avoids field redundancy and data leakage, facilitating evolution and collaboration.

ModuleDescriptionExamples
entityMaps database structure, used for SQLx queriesUserWithRolesEntity
dtoReceives frontend input parameters, field validationCreateUserDto, UserQueryDto
voReturns fields needed for frontend display, secure desensitizationUserListVo, UserDetailVo

IV. Authentication & Authorization: JWT + Server-side Permission Cache

System Overview:

4.1 Middleware Architecture

Core Middleware Components

Middleware Implementation Example

// JWT authentication middleware - only responsible for identity verification
pub async fn jwt_auth_middleware(
    mut request: Request,
    next: Next,
) -> Result<Response, AppError> {
    let token = extract_token_from_header(&request)?;
    // JWT contains only user identity information
    let user_claims = verify_jwt_claims(&token)?;
    request.extensions_mut().insert(CurrentUser { id: user_claims.user_id });
    Ok(next.run(request).await)
}

// Permission middleware - checks permissions in the server-side cache
pub async fn permission_middleware(
    request: Request,
    next: Next,
    permissions: PermissionsCheck,
) -> Result<Response, AppError> {
    let current_user = request.extensions().get::<CurrentUser>()
        .ok_or(ServiceError::InvalidToken)?;
    // Look up permissions in the server-side cache using user ID
    let has_permission = PermissionService::check_permissions(current_user.id, &permissions).await?;
    if !has_permission {
        return Err(ServiceError::PermissionDenied.into());
    }
    Ok(next.run(request).await)
}

Declarative Permission Usage Example

.route_with_permission(
    "/",
    get(get_user_list),
    PermissionsCheck::Any(vec!["system:*", "system:user:*", "system:user:list"]),
)

4.2 Permission Cache and Renewal Mechanism

Why design it this way?

Permission caching reduces database pressure, the renewal mechanism ensures an active user experience, and automatic expiration cleanup enhances security.

How it works:

Permission Cache Implementation Example

// Permission cache manager
pub struct PermissionCacheManager {
    cache: Arc<RwLock<HashMap<i64, UserPermissionCache>>>,
}

impl PermissionService {
    pub async fn check_permissions(
        user_id: i64,
        permissions_check: &PermissionsCheck,
    ) -> Result<bool, ServiceError> {
        // Check if cache exists
        if let Some(cache) = PERMISSION_CACHE.get(user_id) {
            // Check if cache is expired
            if cache.is_expired() {
                PERMISSION_CACHE.remove(user_id);
                return Err(ServiceError::InvalidToken);
            }
            // Check permissions
            let has_permission = permissions_check.check(&cache.permissions);
            return Ok(has_permission);
        }
        Err(ServiceError::InvalidToken)
    }
}

// Permission cache renewal - implemented in AuthService
impl AuthService {
    // Get user login information
    #[tracing::instrument(name = "get_login_info", skip(pool))]
    pub async fn get_login_info(
        pool: &PgPool,
        user_id: i64,
    ) -> Result<UserInfoResponse, ServiceError> {
        // ...omitted...
        // Refresh user permission cache
        Self::refresh_user_permissions_cache(user_id, user.is_super_admin, permissions).await?;
        Ok(user_info)
    }

    pub async fn refresh_user_permissions_cache(
        user_id: i64,
        is_super_admin: bool,
        permissions: Vec<String>,
    ) -> Result<(), ServiceError> {
        if is_super_admin {
            // Super admin permission cache
            PermissionService::cache_user_permissions(user_id, vec!["*".to_string()]);
            return Ok(());
        }
        // Cache user permissions
        PermissionService::cache_user_permissions(user_id, permissions.clone());
        Ok(())
    }
}

4.3 Permission Granularity and Declaration

Note: The current project does not require more fine-grained permission control; the current granularity meets business requirements.


4.4 Performance Optimization Strategies


V. Error Handling and Unified Response

Why design it this way?

Unified error types and response structures improve frontend-backend collaboration efficiency, facilitating AI automatic generation of API documentation and test cases.

#[derive(Debug, thiserror::Error)]
pub enum ServiceError {
    #[error("User is disabled")]
    UserIsDisabled,
    #[error("Username already exists")]
    UsernameConflict,
    #[error("Database query failed")]
    DatabaseQueryFailed,
    #[error("{0} not found")]
    NotFound(String),
    #[error("Insufficient permissions")]
    PermissionDenied,
    #[error("Invalid or expired token")]
    InvalidToken,
    // ... more business error types
}

Error Conversion Mechanism

impl From<ServiceError> for AppError {
    fn from(err: ServiceError) -> Self {
        let (status, code, message) = match err {
            ServiceError::NotFound(resource) => (
                StatusCode::NOT_FOUND,
                10001,
                format!("{} not found", resource),
            ),
            ServiceError::UsernameConflict => (
                StatusCode::CONFLICT,
                10201,
                "Username already exists".to_string(),
            ),
            ServiceError::DatabaseQueryFailed => (
                StatusCode::INTERNAL_SERVER_ERROR,
                20001,
                "Service temporarily unavailable, please try again later".to_string(),
            ),
            ServiceError::InvalidToken => (
                StatusCode::UNAUTHORIZED,
                30000,
                "Invalid or expired token, please login again".to_string(),
            ),
            // ... complete error mapping
        };
        AppError((status, code, message))
    }
}

VI. Response Format: Two Schemes Unified Processing

Scheme One: Simple Response Format

// Single object or non-paginated response
ApiResponse::success(data)

// Response JSON:
{
  "code": 0,
  "message": "Success",
  "data": {
    "id": 1,
    "username": "admin"
  }
}

Scheme Two: Standard Pagination Format

// Paginated list interface
ApiResponse::page(data, total)

// Response JSON:
{
  "code": 0,
  "message": "Success",
  "data": [...],
  "total": 100
}
// In router.rs
pub async fn get_user_list(...) -> AppResult<Vec<UserListVo>> {
    let (users, total) = UserService::get_user_list(&pool, query).await?;
    Ok(ApiResponse::page(users, total))  // Pagination format
}

pub async fn get_user_by_id(...) -> AppResult<UserDetailVo> {
    let user = UserService::get_user_by_id(&pool, id).await?;
    Ok(ApiResponse::success(user))  // Simple format
}

VII. Database Migration Strategy: Zen-Style Numbering System

TypeExample NumberDescription
Table Structure100100 ~ 100500Users, roles, menus, etc. basic tables
Foreign Keys / Triggers100800, 100900Table constraints, log archiving
Views1070xxAggregate views, such as user permissions
Functions1080xxQuery encapsulation, login, permission calculation
Data Seeds1090xxInitialize users, menus, dictionary data, etc.

VIII. Module Dependency Relationship Management

Clear Module Boundaries

// In system/mod.rs
pub fn system_routes() -> Router<PgPool> {
    Router::new()
        .nest("/users", user_routes())
        .nest("/menus", menu_routes())
        .nest("/roles", role_routes())
        .nest("/dicts", dict_routes())
        .nest("/logs", log_routes())
}

Facing complexity, we choose clarity; Facing redundancy, we choose necessity; Facing chaos, we choose Zen.

Rustzen is not a framework, but a way of thinking. I hope it’s written not just for the current me, but also for those of you who are also on the road in the future.

IX. AI-Assisted Programming: The More Zen the Structure, the Higher the Efficiency

X. Conclusion: Rust + Zen, Minimalism is Not Less, But Just Right

Rustzen is an architectural exploration from chaos to order.

Minimalism is not about “doing less”, but about simple design, just right; it provides a clear foundation for AI collaboration, teamwork, and future expansion.

Architecture Advantages Summary


Future Improvement Directions


XI. Closing Message: A Growing Project, Welcome Resonance

Rustzen is the crystallization of my practice from frontend to backend exploration. It’s not an endpoint, but a path to order and clarity.

May every developer find their own balance and direction in the pursuit of “just right”.

Welcome feedback and co-creation, let Rustzen become more developers’ choice of “just right”!