
In today’s fast-paced backend development landscape, Rust is gaining traction for its performance, safety, and growing ecosystem. In this blog series, we’ll walk through how to build a modern, scalable, and maintainable API using Axum, SeaORM, and Rust. We'll use my open-source CMS API project, my-cms, as the reference implementation.
This first post focuses on setting up the project from scratch: initializing the Rust project, configuring dependencies, and bootstrapping a basic HTTP server with Axum.
Before we begin, ensure you have the following installed on your system:
Start by creating a new project using Cargo:
cargo new my-cms-api cd my-cms-api
This generates the default Rust project structure:
my-cms-api/
├── Cargo.toml
└── src
└── main.rs
We’ll be expanding this structure as we go.
Open Cargo.toml and add the following dependencies:
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
sea-orm = { version = "0.12", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }
dotenvy = "0.15"
tracing = "0.1"
tracing-subscriber = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
axum – Web framework built on hyper, designed for modular, type-safe APIs.tokio – Asynchronous runtime that powers most of the ecosystem.sea-orm – Async ORM that works great with PostgreSQL, MySQL, or SQLite.dotenvy – Loads environment variables from a .env file (we’ll use this for configuration).tracing & tracing-subscriber – For structured, async-compatible logging.serde & serde_json – For (de)serializing data structures in JSON.Let’s get our server running. Open src/main.rs and write this basic Axum app:
use axum::{routing::get, Router};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// Initialize router with a single route
let app = Router::new().route("/", get(root_handler));
// Define server address
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("🚀 Server running at http://{}", addr);
// Start server
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// Basic handler
async fn root_handler() -> &'static str {
"Hello, Rust + Axum!"
}
Run the server with:
cargo run
Visit http://localhost:3000 and you should see Hello, Rust + Axum!.
To keep things clean and modular, we’ll split concerns early. Let’s create some folders:
mkdir -p src/{routes,handlers,config,database,models}
touch src/routes/mod.rs src/handlers/mod.rs
Example of modular routing (src/routes/mod.rs):
use axum::{Router, routing::get};
use crate::handlers::health::health_check;
pub fn create_routes() -> Router {
Router::new().route("/health", get(health_check))
}
Example handler (src/handlers/health.rs):
pub async fn health_check() -> &'static str {
"OK"
}
Update src/main.rs to use these:
mod routes;
mod handlers;
use routes::create_routes;
#[tokio::main]
async fn main() {
let app = create_routes();
// ... rest is same
}
.env Configuration #Create a .env file in the root:
DATABASE_URL=postgres://postgres:password@localhost:5432/mycms
Load it at runtime with dotenvy:
use dotenvy::dotenv;
#[tokio::main]
async fn main() {
dotenv().ok();
// ...
}
You now have a structured, minimal web server using Axum, ready to evolve into a full API. In the next post (Part 2), we’ll dive into designing a clean architecture using Axum’s routing system, integrating SeaORM for persistence, and separating concerns via modules.
Want a sneak peek? Check out the repo: https://github.com/doitsu2014/my-cms
Let me know when you're ready to work on Part 2, or if you want a Markdown export of this post.
Published on 4/8/2025