Master Routing with Axum in Rust: Defining Endpoints for Web APIs
If you're building a web application in Rust, you'll likely encounter Axum, a fast and flexible framework built on top of Tokio. One of the most important tasks when building APIs is defining routes. These routes map HTTP requests to specific handler functions, determining what happens when a user accesses a particular URL.
In this article, we’ll dive into routing with Axum. We’ll start with a simple example and gradually enhance it, making sure you understand the key concepts and how to define clean and effective endpoints.
By the end, you'll have the knowledge to start defining your own endpoints and build a basic web API.
Step 1: Set Up Your Project
First, let's create a new Rust project:
cargo new axum-routing
cd axum-routing
Open Cargo.toml
and add the dependencies required for Axum, Tokio, and Serde:
[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Here’s a quick breakdown of the dependencies:
- Axum is the framework we're using for routing.
- Tokio is an asynchronous runtime that Axum depends on.
- Tower is for middleware, which you can add later for additional functionality like logging or error handling.
- Serde is a serialization/deserialization library. It's used here to work with data (such as JSON) between the server and clients.
- Serde_json allows us to work with JSON data.
Run cargo build
to install these dependencies.
Step 2: Write Your First Axum Server
Let’s start by creating a simple Axum server that listens on port 3000 and responds with "Hello, World!" when you visit the root route (/
).
use axum::{Router, routing::get};
#[tokio::main]
async fn main() {
// Create the Axum router with a route for the root path
let app = Router::new().route("/", get(root));
// Start the server on 0.0.0.0:3000
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
// Handler function for the root route
async fn root() -> &'static str {
"Hello, World!"
}
Key Concepts and Explanations
-
Async Main:
The#[tokio::main]
macro tells Rust to run themain
function inside an asynchronous runtime. Axum’s operations, like handling requests, are asynchronous. -
Router:
TheRouter::new()
creates a new Axum router instance. The.route("/", get(root))
maps the root URL (/
) to theroot
function, which is our handler. -
Handler:
A handler is simply an asynchronous function that returns a response. Here, theroot
function sends a "Hello, World!" message when the root route is accessed.
Run the server with cargo run
, then visit http://localhost:3000
to see the "Hello, World!" message.
Step 3: Adding More Routes
Now that we have a basic server, let’s add more routes and introduce more complex handling.
Here’s an updated version that adds additional routes for greeting a user and submitting data with a POST request.
use axum::{Router, routing::{get, post}};
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
#[tokio::main]
async fn main() {
// Define routes for the root, greet, and submit
let app = Router::new()
.route("/", get(root)) // GET at "/"
.route("/greet", get(greet)) // GET at "/greet"
.route("/submit", post(submit)); // POST at "/submit"
// Start the server on 0.0.0.0:3000
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
// Handler for root path
async fn root() -> &'static str {
"Hello, World!"
}
// Handler for /greet
async fn greet() -> &'static str {
"Welcome to Axum!"
}
// Struct to represent data sent via POST request
#[derive(Serialize, Deserialize)]
struct FormData {
name: String,
age: u32,
}
// Handler for /submit
async fn submit(form: axum::extract::Json<FormData>) -> (StatusCode, String) {
let message = format!("Received: {} (Age: {})", form.name, form.age);
(StatusCode::OK, message)
}
Key Concepts and Explanations
-
Multiple Routes:
We now have three routes:"/"
: Returns "Hello, World!""/greet"
: Returns a greeting message."/submit"
: Accepts a POST request with JSON data, using thesubmit
handler.
-
Handling JSON:
Thesubmit
handler usesaxum::extract::Json<FormData>
to extract JSON data sent in the body of the request. TheFormData
struct is defined using Serde for serialization and deserialization of the JSON. -
Response:
Thesubmit
handler returns both a status code and a message. This is done by returning a tuple:(StatusCode::OK, message)
. -
Status Code:
We’re usingStatusCode::OK
to return a successful response. You can explore other status codes likeBadRequest
orNotFound
based on different scenarios.
Step 4: Try Modifying the Code
At this point, you have a functional Axum web server with several routes. Here are some challenges you can try to enhance the app further:
-
Add a Query Parameter:
Modify the/greet
route to accept a query parameter (e.g.,?name=John
) and personalize the greeting. -
Handle Invalid Data:
Modify thesubmit
handler to check if thename
orage
fields are missing or invalid, and return an appropriate error message. -
Experiment with Different HTTP Methods:
Try adding a route forPUT
orDELETE
requests and experiment with how data is sent and handled.
Recap and Conclusion
In this article, we covered:
- How to set up an Axum project and define a basic web server.
- How to add multiple routes, handle GET and POST requests, and extract data from requests.
- How to return responses with status codes and dynamic content.
We also walked through the key concepts of routing in Axum, including:
- Handlers: Functions that respond to specific HTTP methods and routes.
- Routes: How URLs are mapped to handlers.
- Extractors: How Axum pulls data from requests, like JSON or query parameters.
- Serde: A Rust crate for serializing and deserializing data, such as JSON.
With this foundation, you're ready to start building more complex APIs and exploring other features of Axum, such as middleware, authentication, and error handling.
Happy coding!