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

  1. Async Main:
    The #[tokio::main] macro tells Rust to run the main function inside an asynchronous runtime. Axum’s operations, like handling requests, are asynchronous.

  2. Router:
    The Router::new() creates a new Axum router instance. The .route("/", get(root)) maps the root URL (/) to the root function, which is our handler.

  3. Handler:
    A handler is simply an asynchronous function that returns a response. Here, the root 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

  1. Multiple Routes:
    We now have three routes:

    • "/": Returns "Hello, World!"
    • "/greet": Returns a greeting message.
    • "/submit": Accepts a POST request with JSON data, using the submit handler.
  2. Handling JSON:
    The submit handler uses axum::extract::Json<FormData> to extract JSON data sent in the body of the request. The FormData struct is defined using Serde for serialization and deserialization of the JSON.

  3. Response:
    The submit handler returns both a status code and a message. This is done by returning a tuple: (StatusCode::OK, message).

  4. Status Code:
    We’re using StatusCode::OK to return a successful response. You can explore other status codes like BadRequest or NotFound 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:

  1. Add a Query Parameter:
    Modify the /greet route to accept a query parameter (e.g., ?name=John) and personalize the greeting.

  2. Handle Invalid Data:
    Modify the submit handler to check if the name or age fields are missing or invalid, and return an appropriate error message.

  3. Experiment with Different HTTP Methods:
    Try adding a route for PUT or DELETE 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!