📦 GitHub:https://github.com/idaibin/rustzen-admin
📝 Blog:https://idaibin.dev
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:
- Use minimal structure to express clearest intent
- Use simplest path to build sustainable systems
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:
- Early handler layer structure was loose, unclear responsibilities, routes and logic stacked, leading to maintenance difficulties;
- Lack of clear data transmission specifications, logic duplication, increased testing costs;
- In collaboration, due to unclear code structure, generated content was redundant and difficult to combine and reuse.
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.
| Layer | Responsibility Description | Core Features | 
|---|---|---|
| router | Route definition, permission binding, HTTP processing, intuitive functional entry | Router + Handler merged | 
| service | Aggregate business logic, focus on behavior, encapsulate processing flow | Pure business logic, no framework dependency | 
| repo | Database access, encapsulate SQL queries | First-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.
| Module | Description | Examples | 
|---|---|---|
| entity | Maps database structure, used for SQLx queries | UserWithRolesEntity | 
| dto | Receives frontend input parameters, field validation | CreateUserDto,UserQueryDto | 
| vo | Returns fields needed for frontend display, secure desensitization | UserListVo,UserDetailVo | 
- Use impl From<Entity> for VOto implement data conversion
- Clear module responsibilities, facilitating AI and multi-person collaboration understanding and calling
IV. Authentication & Authorization: JWT + Server-side Permission Cache
System Overview:
- Users first authenticate by logging in with their username and password. Upon successful authentication, the server issues a JWT containing only user identity information (such as user ID and username), not permissions.
- All subsequent requests include the JWT; the server verifies the user's identity via the JWT.
- User permission information is stored in a server-side local cache (permission cache) and is looked up and checked as needed.
- The permission cache mechanism reduces database pressure and improves system response speed.
4.1 Middleware Architecture
Core Middleware Components
- ✅ JWT Authentication Middleware: Verifies the token and extracts user identity information
- ✅ Permission Verification Middleware: Looks up and checks permissions in the server-side cache using the user ID
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.
- ✅ Local memory cache: Reduces database queries
- ✅ Automatic cache expiration cleanup: 1-hour expiration, requires re-authorization
- ✅ Permission renewal mechanism: Each time the user info API is accessed, the user's permission cache is refreshed
- ✅ User status changes: When a user is disabled or forcibly logged out, the cache is immediately cleared
- ⚙️ Future extension: Plan to implement a refresh token mechanism for improved security
How it works:
- When a user makes a request, the server first verifies the JWT and extracts basic user info (ID, username)
- The server then looks up the user's permissions in the server-side cache using the user ID
- Each time the user info API is called, the permission cache for that user is automatically refreshed
- The JWT token itself does not yet support refresh; this is planned for future implementation
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
- Permission granularity: The current project's permission granularity is sufficiently fine
- 
Permission codes: Such as system:user:list,system:user:create, etc.
- 
Wildcard support: system:*supports module-wide permissions
Note: The current project does not require more fine-grained permission control; the current granularity meets business requirements.
4.4 Performance Optimization Strategies
- Connection pool management: Configurable connection limits and timeout settings
- Prepared statements: Automatically handled through SQLx
- Efficient pagination: Use offset/limit for pagination queries
- Index optimization: Build indexes for commonly queried fields
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.
- ServiceError + AppResult unified error handling
- ApiResponse unified response format
#[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))
    }
}
- All processing logic uniformly returns AppResult<T>,AppResulthas built-inJson<ApiResponse<T>>, simplifying return types
- Interface layer directly .await?destructures errors, no redundant processing needed
- Error response structure is unified:
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
| Type | Example Number | Description | 
|---|---|---|
| Table Structure | 100100~100500 | Users, roles, menus, etc. basic tables | 
| Foreign Keys / Triggers | 100800,100900 | Table constraints, log archiving | 
| Views | 1070xx | Aggregate views, such as user permissions | 
| Functions | 1080xx | Query encapsulation, login, permission calculation | 
| Data Seeds | 1090xx | Initialize users, menus, dictionary data, etc. | 
- ✅ Modular, traceable, excellent AI maintainability
- ⚠️ Many files, consider merging files when reaching milestones later
- Each file contains a single entity (view, function, or seed), conforming to Zen style
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
- Clear structure allows AI to automatically generate modules (router/dto/service)
- Unified naming, clear logic, facilitating prompt generation and batch refactoring
- Clear module responsibilities, reducing error and rewrite costs
- Log content consistent with function names, facilitating AI understanding and debugging
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
- ✅ Simplicity: Three-layer architecture reduces cognitive load
- ✅ AI-Friendly: Clear separation of concerns, facilitating AI assistance
- ✅ Type Safety: Strong type system, compile-time guarantees
- ✅ High Performance: Caching reduces database load, permission renewal mechanism
- ✅ Maintainability: Single responsibility principle, clear error handling
- ✅ Security: Proper cache expiration mechanism, requires re-authorization when cache expires
Future Improvement Directions
- 🔄 Token Mechanism: Implement refresh token support
- 🔄 Complete Feature Modules: After initial release, complete feature modules based on feedback
- 🔄 Monitoring Metrics: Add performance monitoring and metric collection
- 🔄 Cache Optimization: Consider Redis distributed caching for production environment
- 🔄 Documentation Improvement: More comprehensive documentation, introducing related design and usage instructions
- 🔄 Log Optimization: Optimize log format
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"!
- 📦 Project source code: https://github.com/idaibin/rustzen-admin
- 📝 More hands-on articles: https://idaibin.dev
 

 
    
Top comments (0)