Debug School

rakesh kumar
rakesh kumar

Posted on

Rust Backend Folder Structure Explained: main.rs, Handlers, DB Layer and Architecture Diagram

Why Rust Backend Needs Layers
Architecture Diagram (Request Flow)
Folder Structure (Practical Example)
Complete Layer Mapping (Very Simple Table)
Final Summary (How to Remember)

When you build a Rust backend (Actix-Web + DB), the biggest confusion for many developers is:

Where routes are defined

Where controller logic lives

Where SQL/database code should go

How everything connects in one clean flow

Enter fullscreen mode Exit fullscreen mode

A good folder structure solves all of this. It keeps your code scalable, readable, and easy to maintain.

Why Rust Backend Needs Layers

In a production backend, you don’t want to mix everything in one file.

A layered structure gives you:

Separation of concerns: HTTP logic stays in handlers, SQL stays in DB layer

Scalability: more features → add new module file, not “mess up” existing code

Testability: you can test DB functions separately from handlers

Maintainability: code is predictable for any new developer joining

Most Rust Actix backends follow a flow like:

main.rs starts the server + registers routes + adds middleware

handlers/ contains controller functions for each route

db/ contains query/repository functions that talk to database

models.rs contains structs used across layers

config.rs loads .env and creates typed config

Architecture Diagram (Request Flow)

┌──────────────────────────────┐
│ Client │
│ (Browser / App / Postman) │
└───────────────┬──────────────┘
│ HTTP Request

┌──────────────────────────────┐
│ main.rs │
│ - loads .env + config │
│ - builds DB pool │
│ - registers routes (/api) │
│ - attaches middleware │
└───────────────┬──────────────┘
│ routes to handler

┌──────────────────────────────┐
│ handlers/ (Controller) │
│ - validate input │
│ - call db layer functions │
│ - return JSON response │
└───────────────┬──────────────┘
│ calls repository

┌──────────────────────────────┐
│ db/ (Repository) │
│ - SQL queries (select/insert)│
│ - returns rows as models │
└───────────────┬──────────────┘
│ uses pool

┌──────────────────────────────┐
│ Database │
│ MySQL / Postgres / SQLite │
└──────────────────────────────┘

Shared modules:

- config.rs  (app settings)
- models.rs  (structs for DB + request/response)
- db.rs      (re-export hub / DB layer entry)
Enter fullscreen mode Exit fullscreen mode

Folder Structure (Practical Example)

src/
  main.rs
  config.rs
  models.rs
  handlers.rs
  handlers/
    auth.rs
    shop.rs
    vehicle.rs
  db.rs
  db/
    common.rs
    shop.rs
    vehicle.rs
Enter fullscreen mode Exit fullscreen mode

Each Layer Purpose + Coding Example

Below is how each layer works with small realistic code examples.

Layer A — main.rs (Application Entry + Wiring)
Purpose

main.rs is responsible for:

loading .env

initializing config

connecting database

creating shared state (AppState)

registering routes

attaching middleware (CORS, sessions)

serving static files
Enter fullscreen mode Exit fullscreen mode

Example (short and clear)

mod config;
mod db;
mod handlers;
mod models;

use actix_web::{App, HttpServer, web};
use dotenvy::dotenv;
use config::AppConfig;
use db::{init_pool, state_data, AppState};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();

    let cfg = AppConfig::from_env();
    let pool = init_pool(&cfg.database_url).await.expect("DB failed");

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

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .service(
                web::scope("/api")
                    .route("/shops", web::get().to(handlers::list_shops_handler))
            )
    })
    .bind(cfg.bind_addr)?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Layer B — handlers/ (Controller Layer)
Purpose

Handlers are like controllers:

receive request

validate input

call DB layer functions

return JSON/HTTP responses
Enter fullscreen mode Exit fullscreen mode

Example handler (handlers/shop.rs)

use actix_web::{web, HttpResponse, Responder};
use crate::db;
use crate::db::AppState;

pub async fn list_shops_handler(state: web::Data<AppState>) -> impl Responder {
    match db::list_shops(&state.pool).await {
        Ok(rows) => HttpResponse::Ok().json(rows),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Notice: handler is NOT writing SQL.
It just calls db::list_shops().

Layer C — db/ (Repository / Query Layer)
Purpose

This is the real DB layer:

executes SQL

maps rows into Rust structs

returns results back to handlers
Enter fullscreen mode Exit fullscreen mode

Example DB function (db/shop.rs)

use sqlx::{MySqlPool};
use crate::models::Shop;

pub async fn list_shops(pool: &MySqlPool) -> Result<Vec<Shop>, sqlx::Error> {
    let rows = sqlx::query_as!(
        Shop,
        r#"SELECT id, name, city FROM shops ORDER BY id DESC"#
    )
    .fetch_all(pool)
    .await?;

    Ok(rows)
}
Enter fullscreen mode Exit fullscreen mode

✅ Here SQL stays in DB layer.
Handlers stay clean.

Layer D — db.rs (DB Layer Entry / Re-export Hub)
Purpose

db.rs acts as a single entry point for all DB functions.

It connects to submodules and re-exports functions so you can call:

db::list_shops() instead of db::shop::list_shops()
Enter fullscreen mode Exit fullscreen mode

Example (db.rs)

pub mod common;
pub mod shop;

pub use common::{init_pool, state_data, AppState};
pub use shop::{list_shops};
Enter fullscreen mode Exit fullscreen mode

This is basically a Facade pattern for DB layer.

Layer E — models.rs (Structs / Data Contracts)
Purpose

Models define structure for:

database row mapping

JSON response

request body validation structs
Enter fullscreen mode Exit fullscreen mode

Example model

use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Shop {
    pub id: i32,
    pub name: String,
    pub city: String,
}
Enter fullscreen mode Exit fullscreen mode

Now DB queries can map into Shop, and handlers can return it as JSON.

Layer F — config.rs (Configuration Loader)
Purpose

Loads env variables and creates typed config
Enter fullscreen mode Exit fullscreen mode

Example

use std::env;

pub struct AppConfig {
    pub database_url: String,
    pub bind_addr: String,
    pub app_name: String,
}

impl AppConfig {
    pub fn from_env() -> Self {
        Self {
            database_url: env::var("DATABASE_URL").expect("DATABASE_URL missing"),
            bind_addr: env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".into()),
            app_name: env::var("APP_NAME").unwrap_or_else(|_| "Rust API".into()),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Complete Layer Mapping (Very Simple Table)

Final Summary (How to Remember)

Just remember this one line:

main.rs wires everything, handlers control requests, db executes SQL.

Flow:

Request → main.rs routes → handlers/ → db/ → database → response
Enter fullscreen mode Exit fullscreen mode

Top comments (0)