Skip to main content

Structuring Code Base

Look at how you can restructure your application using modules and the Ports and Adapters architecture:

Step 1: Identify Your Core Domain

First, identify the core business logic of your application. In your case, it's the user management logic:

// core.rs
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegisterUserRequest {
pub email_address: String,
pub password: String,
pub name: String,
}

#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct UserDetails {
email_address: String,
password: String,
age: Option<i32>,
name: String,
}

#[derive(Clone)]
pub enum User {
Standard { user_details: UserDetails },
Premium { user_details: UserDetails, is_premium: bool }
}

impl User {
pub fn new(email_address: &str, name: &str, password: &str) -> User {
// Implementation
}

pub fn details(&self) -> &UserDetails {
// Implementation
}

// Other methods.
}

Note the use of pub for items that need to be accessible from other modules.

Step 2: Define Data Access Layer

Next, create a separate module for data access concerns:

// data_access.rs
use std::sync::{Arc, RwLock};
use crate::core::User;

#[derive(Default)]
pub struct AppState {
pub(crate) users: Vec<User>,
}

pub type SharedState = Arc<RwLock<AppState>>;

The pub(crate) modifier makes users accessible only within the current crate, not to external users of the library.

Step 3: Wire Everything in the Main Module

Finally, use both modules in your main application:

// main.rs
mod core;
mod data_access;

use crate::core::{LoginRequest, RegisterUserRequest, User, UserDetails};
use crate::data_access::SharedState;
use axum::{/* . */};

#[tokio::main]
async fn main() {
// Create a new router
let app = Router::new()
.route("/users", post(register_user))
// Other routes.
.with_state(SharedState::default());

// Start the server.
}

async fn register_user(
State(state): State<SharedState>,
Json(payload): Json<RegisterUserRequest>,
) -> (StatusCode, Json<UserDetails>) {
let user = User::new(&payload.email_address, &payload.name, &payload.password);
state.write().unwrap().users.push(user.clone());

(StatusCode::CREATED, Json(user.details().clone()))
}

// Other handler functions.
info

Note the use of use crate::core::{LoginRequest, RegisterUserRequest, User, UserDetails}; in the imports at the top of the file. crate:: is a special import syntax to import things from over crates in the same package

The Benefits of This Structure

This modular approach offers you several benefits:

  1. Separation of Concerns: Business logic is isolated from infrastructure details
  2. Testability: Core domain logic can be tested independently
  3. Flexibility: External systems can be replaced without changing the core
  4. Maintainability: Clear boundaries make the codebase easier to understand
  5. Reusability: Core logic can be reused across different interfaces

Comparing with .NET

Concept.NETRust
Organizing CodeNamespaces, AssembliesModules, Crates
Access Controlpublic, internal, privatepub, pub(crate), no modifier
Project Structure.csproj filesCargo.toml
Interface Definitioninterfacestraits
Dependency InversionDI ContainerTrait objects, Generics