Debug School

rakesh kumar
rakesh kumar

Posted on

Rust main.rs Explained: Environment Loading, Database Connection, Routing, and Middleware Setup

Why main.rs is so important

Purpose + Layer Architecture Diagram

Code Walkthrough: Explain Each Part of Your main.rs

Module wiring (connect project layers)
How Rust Module System Works (Important)
How db.rs Connects to db Folder
Imports (what features your server will use)
Actix runtime macro (async main support)
Load environment variables
Create upload folders (file system boot step)
Initialize configuration from environment
Database connection + Pool creation
Startup normalization tasks (safe cleanup at boot)
Build shared application state (DB pool + app_name)
Session key setup (secure cookie signing)
Create the server (App factory per worker)
Configure CORS (frontend access)
Build Actix App + inject state + attach middleware
Register routes (API endpoints)
Serve static files (/uploads)
Bind + run server (final start)

Mini Coding Example: One Complete “Layer Flow” (main.rs → handler → db)

Final Takeaway

main.rs is the boot file of your Rust backend. In an Actix-Web project, it works like the “control room” where you:

load environment variables (.env)

initialize configuration

connect to database (create pool)

prepare shared app state

attach middleware (CORS, sessions)

register routes (/api/...)

serve static files (/uploads)

start the HTTP server
Enter fullscreen mode Exit fullscreen mode

If handlers are the “controllers,” then main.rs is the “application bootstrap + router + server starter.”

Why main.rs is so important

In Rust (especially backend), the app is usually designed in layers:

Bootstrap Layer (main.rs) – starts everything

Controller Layer (handlers) – HTTP request/response logic

Data Layer (db) – queries + database logic

Models Layer (models) – structs shared across layers

Config Layer (config) – typed settings from env

The goal: keep each responsibility separated so the project stays clean even when it grows.

Purpose + Layer Architecture Diagram

Code Walkthrough: Explain Each Part of Your main.rs

Below I’m explaining your exact code section by section and mapping it to the purpose.

A) Module wiring (connect project layers)

mod config;
mod db;
mod handlers;
mod layout;
mod models;
Enter fullscreen mode Exit fullscreen mode

Role: This connects your folder structure to Rust compiler.

mod db; loads src/db.rs (and inside it, src/db/*)

mod handlers; loads src/handlers.rs (and/or src/handlers/*)

mod models; loads shared structs

mod config; loads typed config
Enter fullscreen mode Exit fullscreen mode

layout is commonly used for SSR templates/layout helpers

This is the first “wiring step” in Rust.

How Rust Module System Works (Important)


How db.rs Connects to db Folder

B) Imports (what features your server will use)

use actix_cors::Cors;
use actix_files::Files;
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::{
    cookie::{Key, SameSite},
    web, App, HttpServer,
};
use config::AppConfig;
use db::{init_pool, normalize_brand_model_rows, normalize_shop_rows, state_data, AppState};
use dotenvy::dotenv;
Enter fullscreen mode Exit fullscreen mode

Role (why each is needed):

Cors → allow your frontend domains to call your API

Files → serve static /uploads URLs

SessionMiddleware + CookieSessionStore → login session using cookies

Key + SameSite → cookie signing + cookie policy

web → scopes (/api), route mapping, shared state

App / HttpServer → create Actix app + run server

AppConfig → read config from env

init_pool → connect DB

normalize_* → startup data cleanup tasks

state_data + AppState → share DB pool with handlers

dotenv → load .env

C) Actix runtime macro (async main support)

#[actix_web::main]
async fn main() -> std::io::Result<()> {
Enter fullscreen mode Exit fullscreen mode

Role: #[actix_web::main] is an attribute macro that:


starts Actix/Tokio runtime

allows async fn main()

allows .await inside main
Enter fullscreen mode Exit fullscreen mode

Without this, async fn main() won’t run.

D) Load environment variables

dotenv().ok();
Enter fullscreen mode Exit fullscreen mode

Role: Loads .env file values into std::env.

Now DATABASE_URL, SESSION_KEY, etc. can be read.
Enter fullscreen mode Exit fullscreen mode

This matches: ✅ Environment Loading

E) Create upload folders (file system boot step)

std::fs::create_dir_all("uploads/brand_models")?;
std::fs::create_dir_all("uploads/insurance")?;
std::fs::create_dir_all("uploads/pollution")?;
std::fs::create_dir_all("uploads/rc_document")?;
Enter fullscreen mode Exit fullscreen mode

Role: Ensures directories exist before uploads happen.
If they don’t exist and you upload a file, your upload handler would fail.

Matches: ✅ Creates upload folders

F) Initialize configuration from environment

let cfg = AppConfig::from_env();
Enter fullscreen mode Exit fullscreen mode

Role: Reads env vars and builds a typed config object (AppConfig).

Typical values:

cfg.database_url

cfg.bind_addr

cfg.app_name
Enter fullscreen mode Exit fullscreen mode

Matches: ✅ Initialize configuration

G) Database connection + Pool creation

let pool = init_pool(&cfg.database_url)
    .await
    .expect("Database connection failed");

Enter fullscreen mode Exit fullscreen mode

Role: Creates a DB connection pool.

Pool = reusable database connections

Helps performance (no reconnect on every request)

Shared across handlers using AppState

Matches: ✅ Connect to database / ✅ Build DB pool

H) Startup normalization tasks (safe cleanup at boot)

if let Err(err) = normalize_brand_model_rows(&pool).await {
    eprintln!("[startup] normalize_brand_model_rows failed: {err}");
}
if let Err(err) = normalize_shop_rows(&pool).await {
    eprintln!("[startup] normalize_shop_rows failed: {err}");
}
Enter fullscreen mode Exit fullscreen mode

Role: Runs startup tasks that may:

fix bad/empty data

normalize columns (case/spacing)

ensure consistent rows
Enter fullscreen mode Exit fullscreen mode

Important: you do not stop server if these fail—only print error.
So server still boots.

I) Build shared application state (DB pool + app_name)

let state = state_data(AppState {
    pool,
    app_name: cfg.app_name.clone(),
});
Enter fullscreen mode Exit fullscreen mode

Role: This creates a shared state container that will be injected into all handlers.

AppState holds pool + other shared values

state_data(...) likely wraps into web::Data<AppState>

Later used in handlers as: state: web::Data<AppState>

This is how handlers get DB pool without global variables.
Enter fullscreen mode Exit fullscreen mode

J) Session key setup (secure cookie signing)

let session_key = std::env::var("SESSION_KEY")
    .unwrap_or_else(|_| "rust-crud-dev-session-key-please-change".to_string());
let mut session_key_bytes = session_key.into_bytes();
if session_key_bytes.len() < 32 {
    session_key_bytes.resize(32, b'0');
}
Enter fullscreen mode Exit fullscreen mode
let key = Key::derive_from(&session_key_bytes);
Enter fullscreen mode Exit fullscreen mode

Role:

Reads SESSION_KEY

Ensures minimum length (32 bytes)

Creates Key to sign session cookies (prevents tampering)
Enter fullscreen mode Exit fullscreen mode

In production:

you must set a strong random SESSION_KEY

do not keep dev fallback key
Enter fullscreen mode Exit fullscreen mode

Matches: ✅ Middleware setup (Session)

K) Create the server (App factory per worker)

HttpServer::new(move || {
Enter fullscreen mode Exit fullscreen mode

Role: Starts Actix server.
The closure builds an App instance for each worker thread.

move allows using state and key inside.

Matches: ✅ Start HTTP server

L) Configure CORS (frontend access)

let cors = Cors::default()
    .allowed_origin("http://localhost:5173")
    .allowed_origin("http://127.0.0.1:5173")
    ...
    .allow_any_header()
    .allow_any_method()
    .supports_credentials();
Enter fullscreen mode Exit fullscreen mode

Role:

Allows React/Vite origins to call your API

supports_credentials() is critical because you are using cookie-based sessions

allow_any_method() enables GET/POST/PUT/DELETE etc.
Enter fullscreen mode Exit fullscreen mode

Matches: ✅ Middleware setup (CORS)

M) Build Actix App + inject state + attach middleware

App::new()
    .app_data(state.clone())
    .wrap(cors)
    .wrap(
        SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
            .cookie_secure(false)
            .cookie_same_site(SameSite::Lax)
            .build(),
    )
Enter fullscreen mode Exit fullscreen mode

Role:

app_data(state.clone()) → makes AppState available to all handlers

.wrap(cors) → adds CORS middleware

.wrap(SessionMiddleware...) → adds session middleware
Enter fullscreen mode Exit fullscreen mode

Details:


cookie_secure(false) → development mode (HTTP).
In production (HTTPS), set true.

SameSite::Lax → safer cookie behavior while still allowing common flows

Matches: ✅ Attach middleware
Enter fullscreen mode Exit fullscreen mode

N) Register routes (API endpoints)

.service(
    web::scope("/api")
        .route("/login", web::post().to(handlers::login))
        .route("/logout", web::post().to(handlers::logout))
        .route("/me", web::get().to(handlers::me))
        ...
)

Enter fullscreen mode Exit fullscreen mode

Role:

web::scope("/api") means all routes start with /api

Each .route() maps a URL + HTTP method to a handler function
Enter fullscreen mode Exit fullscreen mode

Example mapping:


POST /api/login → handlers::login

GET /api/shops → handlers::list_shops_handler

POST /api/bookings/offline → handlers::save_offline_booking_handler
Enter fullscreen mode Exit fullscreen mode

Matches: ✅ Register routes

O) Serve static files (/uploads)

.service(Files::new("/uploads", "./uploads"))
Enter fullscreen mode Exit fullscreen mode

Role: Makes uploaded files publicly accessible.

Example:

stored path: ./uploads/insurance/a.pdf

URL: /uploads/insurance/a.pdf
Enter fullscreen mode Exit fullscreen mode

Matches: ✅ Serve static files

P) Bind + run server (final start)

})
.bind(cfg.bind_addr)?
.run()
.await
Enter fullscreen mode Exit fullscreen mode

Role:

.bind(cfg.bind_addr) binds to your host/port (example 127.0.0.1:8080)

.run().await keeps server running

Matches: ✅ Start HTTP server
Enter fullscreen mode Exit fullscreen mode

4)

Mini Coding Example: One Complete “Layer Flow” (main.rs → handler → db)

This small example shows how your wiring becomes real.

Route in main.rs

web::scope("/api")
    .route("/shops", web::get().to(handlers::list_shops_handler))
Enter fullscreen mode Exit fullscreen mode

Handler (controller)

pub async fn list_shops_handler(state: web::Data<AppState>) -> impl Responder {
    let shops = db::list_shops(&state.pool).await;
    HttpResponse::Ok().json(shops)
}
Enter fullscreen mode Exit fullscreen mode

DB layer function (repository)

pub async fn list_shops(pool: &MySqlPool) -> Result<Vec<Shop>, sqlx::Error> {
    sqlx::query_as!(Shop, "SELECT id, name FROM shops")
        .fetch_all(pool)
        .await
}
Enter fullscreen mode Exit fullscreen mode

This is the exact layered design your main.rs enables.

Final Takeaway

main.rs is not where your business logic belongs.
Enter fullscreen mode Exit fullscreen mode

It is where you assemble the whole application:

env → config → db → state → middleware → routes → server
Enter fullscreen mode Exit fullscreen mode

That’s why main.rs is called Bootstrap + Composition Root in backend architecture.

If you want, I can rewrite this into a ready-to-publish SEO blog format with headings, beginner-friendly language, and a “common mistakes in main.rs” section (like cookie_secure(false) in production, CORS credential issues, etc.).

Top comments (0)