Create Scalable Axum Web Apps by Organizing Routes and Handlers
Axum is an amazing framework for building web applications in Rust, but as applications grow, code organization becomes critical for maintainability and scalability. In this article, we’ll explore how to structure your Axum app using routes and handlers in separate files. We will take a hands-on approach to progressively modularize your codebase and keep it clean and manageable.
By the end of this article, you’ll understand how to break your application into separate files and modules that handle routing, logic, and utilities.
Step 1: Setting Up the Project
Let’s first set up a minimal Axum project. In your terminal, run the following commands:
cargo new axum_modular_example --bin
cd axum_modular_example
Now, open the Cargo.toml
file and add dependencies for Axum and Tokio:
[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Let’s now create a simple Axum application that listens on port 3000
and responds with "Hello, World!"
when accessed. Open src/main.rs
and write the following code:
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
With this, you’ve created a basic server. To run it:
cargo run
Your app is now serving "Hello, World!"
. But what if this app grows, and you have more routes and business logic? This is where modularity comes into play.
Step 2: Modularizing the Routes and Handlers
Organizing Routes and Handlers
Instead of keeping everything in main.rs
, we’ll split the functionality into separate files. We will:
- Move routes into the
routes
module. - Place handlers in a separate
handlers
module to further separate concerns.
Creating the routes
and handlers
Modules
- First, create the
routes
andhandlers
directories:
mkdir src/routes
mkdir src/handlers
- Inside
src/routes
, create amod.rs
file:
touch src/routes/mod.rs
- Similarly, create the
mod.rs
file insidesrc/handlers
:
touch src/handlers/mod.rs
Moving the Handler Logic
Next, we’ll move the route handlers into the handlers/mod.rs
file.
// src/handlers/mod.rs
use axum::response::IntoResponse;
pub async fn hello_world() -> impl IntoResponse {
"Hello, World!"
}
pub async fn current_time() -> impl IntoResponse {
use std::time::SystemTime;
let time = SystemTime::now();
format!("Current time is: {:?}", time)
}
Now that the handlers are separate, the handlers/mod.rs
file has two functions: hello_world
and current_time
, each returning responses.
Organizing Routes
Now, let’s move the routing logic into routes/mod.rs
to wire everything together. We'll import the handler functions here:
// src/routes/mod.rs
use axum::{routing::get, Router};
use crate::handlers::{hello_world, current_time};
pub fn create_routes() -> Router {
Router::new()
.route("/", get(hello_world))
.route("/time", get(current_time))
}
Here, we define a function create_routes()
that sets up all routes and associates them with their respective handlers from the handlers
module.
Updating main.rs
Finally, update the main.rs
file to use the newly modularized structure:
use axum::Server;
use routes::create_routes;
mod routes;
mod handlers;
#[tokio::main]
async fn main() {
let app = create_routes();
Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
Notice how main.rs
is now much simpler. It just calls create_routes()
from the routes
module, and the routing logic is neatly abstracted away. We’ve now separated the concerns of routing, handlers, and the main entry point.
Step 3: Adding More Complexity with Additional Routes
Now, let's extend our application by adding more routes. For example, let’s introduce a new route that greets users by name. This will demonstrate how we can continue expanding the application while keeping the structure clean.
Adding the greet
Handler
Add the greet
handler to handlers/mod.rs
:
// src/handlers/mod.rs
use axum::extract::Json;
pub async fn greet(Json(payload): Json<GreetRequest>) -> impl IntoResponse {
format!("Hello, {}!", payload.name)
}
#[derive(serde::Deserialize)]
pub struct GreetRequest {
pub name: String,
}
This handler receives a JSON object containing a name
field, and it returns a greeting.
Adding the Route in routes/mod.rs
Now, add a route for the greet
handler:
// src/routes/mod.rs
use axum::{routing::{get, post}, Router};
use crate::handlers::{hello_world, current_time, greet};
pub fn create_routes() -> Router {
Router::new()
.route("/", get(hello_world))
.route("/time", get(current_time))
.route("/greet", post(greet))
}
Testing the New Route
Now, your app can greet users via POST requests to /greet
. You can test it with a tool like Postman or curl
:
curl -X POST http://localhost:3000/greet -H "Content-Type: application/json" -d '{"name": "Alice"}'
This should return:
Hello, Alice!
Step 4: Challenges and Next Steps
Challenge: Add More Routes
As an exercise, try adding another route that handles PUT or DELETE requests. This will help you become familiar with how Axum handles various HTTP methods. For example, you could add a /update
endpoint that updates user information.
pub async fn update_user(Json(payload): Json<UpdateUserRequest>) -> impl IntoResponse {
format!("Updated user with ID: {}", payload.id)
}
#[derive(serde::Deserialize)]
pub struct UpdateUserRequest {
pub id: u32,
}
Next Steps: Explore Middleware and Error Handling
Now that your application is modular, you can begin looking into Axum’s powerful middleware features for adding logging, authentication, or even rate limiting. You can also explore error handling to return meaningful error responses when something goes wrong.
Recap and Conclusion
We’ve covered how to structure an Axum application using Rust’s module system. Here’s what we accomplished:
- Separated handlers and routes into their respective modules for better organization.
- Used Axum’s
Router
to wire everything together and handle different HTTP methods. - Introduced new routes and handlers like
/greet
to demonstrate how to expand the app.
This approach will help keep your Axum applications maintainable as they scale, and it allows for easy navigation through your codebase. By organizing routes, handlers, and utilities into separate files and modules, you'll find that your development workflow becomes smoother and more scalable.
Happy coding, and see you next time!