Soojeong Lee

Jul 29, 2025

Soojeong Lee

Jul 29, 2025

Soojeong Lee

Jul 29, 2025

Backend with Rust axum?

Backend with Rust axum?

Trying Rust Axum

First, the result...?

https://github.com/MontyCoder0701/axum-starter


Why use Rust for the backend?

In fact, there isn't an urgent reason to do it right now. Personally, I think Rust is a good language, and it's much faster than the Node environment. I have a feeling that someday Rust backends will take over, but it seems like it won't change easily because Java Spring has a monopoly.


So what have I done?

Nest.js is super!!! friendly. What I mean is that with just one line of CLI, everything you want is created easily. In contrast, Rust's frameworks have no popular options yet, and the ecosystem isn't very large. This means that you have to build everything you want with craftsmanship.


First, I will share the main code.

use std::env;

use axum::{
    Router,
    http::{
        HeaderValue, Method, StatusCode,
        header::{AUTHORIZATION, CONTENT_TYPE},
    },
};
use deadpool_diesel::{
    Runtime,
    mysql::{Manager, Pool},
};
use dotenvy::dotenv;
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;

mod common;
mod hello;
mod user;

#[tokio::main]
async fn main() {
    dotenv().ok();

    const PORT: u16 = 3200;
    const HOST: &str = "localhost";

    let db_url = env::var("DATABASE_URL").unwrap();
    let client_url = env::var("CLIENT_URL").unwrap();

    let manager = Manager::new(db_url, Runtime::Tokio1);
    let pool = Pool::builder(manager).build().unwrap();

    let cors = CorsLayer::new()
        .allow_origin(client_url.parse::<HeaderValue>().unwrap())
        .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
        .allow_credentials(true)
        .allow_headers([AUTHORIZATION, CONTENT_TYPE]);

    let app = Router::new()
        .merge(hello::controller::routes())
        .merge(user::controller::routes())
        .layer(cors)
        .with_state(pool)
        .fallback((StatusCode::NOT_FOUND, "404 Not Found"));

    let listener = TcpListener::bind(format!("{HOST}:{PORT}")).await.unwrap();

    axum::serve(listener, app).await.unwrap();
}


I created the DB connection, added CORS, and registered the router. It's a very typical flow. The part I've tried to pay attention to is the separation of modules and each layer.

Neither the official documentation nor anything explicitly shows the separation of modules and layers. But shouldn't we do it if we think about maintenance?

So, I tried to establish a structure.
User module was created first. This includes model, schema, service, and controller. The reason I only declared the controller as public is that the only thing exposed outside the module is the router of the controller.

pub mod controller;
mod model;
mod schema;
mod service;


Then I register the route in the controller.

use axum::{Router, routing::get};
use deadpool_diesel::mysql::Pool;

use super::service::get_users;

const PATH: &str = "/users";

pub fn routes() -> Router<Pool> {
    Router::new().route(PATH, get(get_users))
}


Then, the service takes care of the business logic and interacts with the DB.

use axum::{Json, extract::State, http::StatusCode};
use deadpool_diesel::mysql::Pool;
use diesel::prelude::*;

use super::{model::User, schema::user};
use crate::common::utils::internal_error;

pub async fn get_users(State(pool): State<Pool>) -> Result<Json<Vec<User>>, (StatusCode, String)> {
    let conn = pool.get().await.map_err(internal_error)?;

    let users = conn
        .interact(|conn| user::table.select(User::as_select()).load(conn))
        .await
        .map_err(internal_error)?
        .map_err(internal_error)?;

    Ok(Json(users))
}


And I used Diesel, the most famous ORM.

I declare the schema,

diesel::table! {
    user (id) {
        id -> Integer,
        name -> Varchar,
    }
}

and create the corresponding model.

use diesel::{mysql::Mysql, prelude::*};

use super::schema::user;

#[derive(Queryable, Selectable)]
#[diesel(table_name = user)]
#[diesel(check_for_backend(Mysql))]
#[derive(serde::Serialize)]

pub struct User {
    pub id: i32,
    pub name: String,
}


Isn't it a very familiar pattern? But the surprising fact is that you have to make all of this by hand. There's no quick and easy way to do it... Now the next goal is to create proper structs to implement it in other modules.

Anyway... it seems doable..?

Comments

Comments

Comments