Rust & Axum: A Practical Guide to Understanding Requests & Responses
Building web applications in Rust has become easier with frameworks like Axum. Axum is a minimal and flexible web framework built on top of Tokio that focuses on high performance, making it a great choice for building APIs.
In this article, we'll dive into how the request/response model works in Axum, covering JSON handling, request bodies, and headers. We'll build this understanding step by step, starting with a simple example and gradually adding complexity. By the end, you'll have a hands-on understanding of handling HTTP requests and responses in Rust using Axum. Let's get started!
Step 1: Set up the project
First, we need to create a new Rust project and add Axum as a dependency.
- Open your terminal and create a new project:
cargo new axum_demo
cd axum_demo
- Now, modify your
Cargo.toml
file to include the necessary dependencies:
[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
These dependencies are:
axum
for building the web server.tokio
for asynchronous operations.serde
andserde_json
to handle serialization and deserialization of data, especially JSON.
Step 2: Build a basic "Hello, World!" server
Let's start by building a simple Axum server that responds with a basic string message.
Replace the contents of src/main.rs
with the following:
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(root));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> &'static str {
"Hello, World!"
}
Explanation:
Router::new().route("/", get(root))
: This defines the route for handling GET requests to the root path (/
) and links it to theroot
function.axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(app.into_make_service()).await
: This starts the server and listens on port3000
.root()
: This function returns a simple string "Hello, World!" when a request hits the root path.
Run your server using cargo run
. Open your browser or use curl
to visit http://localhost:3000
, and you should see the text "Hello, World!"
Step 3: Work with JSON data
Now that we have a working server, let’s work with JSON. We'll modify the server to return a JSON response instead of a plain string.
Replace the root()
function with this:
use axum::{response::Json};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Message {
message: String,
}
async fn root() -> Json<Message> {
Json(Message {
message: String::from("Hello, World in JSON!"),
})
}
Explanation:
#[derive(Serialize, Deserialize)]
: This macro is used to make theMessage
struct both serializable and deserializable. We useSerialize
to convert the struct to JSON andDeserialize
to convert incoming JSON into the struct.Json<Message>
: TheJson
wrapper ensures the return type is automatically serialized to JSON by Axum.Json(Message { message: String::from("Hello, World in JSON!") })
: This creates an instance ofMessage
and wraps it in aJson
response.
When you visit http://localhost:3000
again, the response will be:
{
"message": "Hello, World in JSON!"
}
Step 4: Handling POST requests and Request Bodies
Next, we will handle POST requests and work with the request body. In a real application, clients might send data (e.g., user input) to your server. We'll accept a JSON payload in a POST request.
Update the main.rs
file to include a new route for POST requests:
use axum::{extract::Json, routing::post};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/send", post(send_message));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
async fn send_message(Json(payload): Json<Message>) -> Json<Message> {
Json(Message {
message: format!("Received: {}", payload.message),
})
}
Explanation:
post(send_message)
: We define a route that listens for POST requests on the/send
path.Json(payload): Json<Message>
: TheJson
extractor automatically deserializes the incoming request body into aMessage
struct.format!("Received: {}", payload.message)
: We modify the message in the response to show what was sent by the client.
Testing POST with curl
:
Use the following curl
command to test the new POST endpoint:
curl -X POST http://localhost:3000/send -H "Content-Type: application/json" -d '{"message": "Hello!"}'
The response will be:
{
"message": "Received: Hello!"
}
This demonstrates how you can process JSON data sent by the client and return a modified response.
Concepts and Explanations
Request/Response Cycle in Axum
In web development, the request/response model is fundamental. Here's what happens in Axum:
-
Request: When a client (e.g., a browser or an API client) sends a request, it includes various components:
- Method: Defines the type of operation (e.g., GET, POST).
- Headers: Provide additional metadata about the request (e.g.,
Content-Type
,Authorization
). - Body: Contains data sent with the request (e.g., JSON, form data).
-
Response: The server sends back a response, which includes:
- Status Code: Indicates whether the request was successful (e.g.,
200 OK
,404 Not Found
). - Headers: Metadata about the response (e.g.,
Content-Type: application/json
). - Body: The data being sent back (e.g., JSON, HTML).
- Status Code: Indicates whether the request was successful (e.g.,
In Axum:
Json<T>
handles JSON serialization and deserialization.extract::Json
is used to extract data from the request body.response::Json
sends back a JSON response.
Challenges or Questions
Now that you’ve built a basic application, try the following challenges to deepen your understanding:
- Add more fields to the
Message
struct and handle those fields in both requests and responses. How does this affect the JSON response? - Set custom HTTP headers. For example, try to add a custom
X-Response-Time
header to your responses. - Deserialize nested JSON objects. Create a more complex data structure (e.g., a
User
struct with aProfile
struct) and practice handling nested JSON.
Recap and Conclusion
In this article, we’ve explored the basics of handling HTTP requests and responses using Rust and Axum. Here's what we covered:
- Setting up a basic server using Axum.
- Returning and accepting JSON data with the
Json
type (using bothSerialize
andDeserialize
). - Handling POST requests and processing request bodies.
- Understanding the request/response cycle in web development.
With these foundational concepts in hand, you're ready to start building more complex APIs and web services in Rust using Axum. Keep experimenting and building—Axum’s simplicity and power make it an excellent choice for Rust web development.
Happy coding!