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