Let’s build a real endpoint in your Axum API. You’ll create a /api/items endpoint that returns a list.

Step 1: Define the Item Type

Create api/src/models.rs:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
    pub id: u32,
    pub name: String,
    pub completed: bool,
}

Add to api/src/main.rs:

mod models;

Step 2: Create a Handler

Create api/src/handlers/items.rs:

use axum::Json;
use crate::models::Item;

pub async fn list_items() -> Json<Vec<Item>> {
    let items = vec![
        Item {
            id: 1,
            name: "Learn Rust".to_string(),
            completed: false,
        },
        Item {
            id: 2,
            name: "Build API".to_string(),
            completed: true,
        },
    ];

    Json(items)
}

Create/update api/src/handlers/mod.rs:

pub mod items;
pub mod health;

pub use items::*;
pub use health::*;

Add to api/src/main.rs:

mod handlers;

Step 3: Register the Route

Update api/src/router.rs:

use axum::{
    routing::get,
    Router,
};
use crate::handlers;

pub fn build(db: SqlitePool) -> Router {
    Router::new()
        .route("/health", get(handlers::health))
        .route("/api/items", get(handlers::list_items))  // NEW
}

Step 4: Test It

Make sure your API is running:

cd api
cargo run

In another terminal, test your endpoint:

curl http://localhost:3001/api/items

Expected response:

[
  {
    "id": 1,
    "name": "Learn Rust",
    "completed": false
  },
  {
    "id": 2,
    "name": "Build API",
    "completed": true
  }
]

Success! 🎉

Step 5: Add a POST Endpoint

Now let’s create items. Add to api/src/handlers/items.rs:

use axum::Json;
use serde::Deserialize;
use crate::models::Item;

#[derive(Deserialize)]
pub struct CreateItemRequest {
    pub name: String,
}

pub async fn create_item(
    Json(payload): Json<CreateItemRequest>,
) -> Json<Item> {
    let item = Item {
        id: 3,
        name: payload.name,
        completed: false,
    };

    Json(item)
}

Add to api/src/router.rs:

use axum::routing::{get, post};

Router::new()
    .route("/health", get(handlers::health))
    .route("/api/items", get(handlers::list_items))
    .route("/api/items", post(handlers::create_item))  // NEW

Test it:

curl -X POST http://localhost:3001/api/items \
  -H "Content-Type: application/json" \
  -d '{"name":"New Item"}'

Response:

{
  "id": 3,
  "name": "New Item",
  "completed": false
}

Step 6: Add Error Handling

Create api/src/handlers/items.rs with GET by ID:

use axum::extract::Path;
use crate::errors::AppError;

pub async fn get_item(
    Path(id): Path<u32>,
) -> Result<Json<Item>, AppError> {
    // Simulate looking up an item
    if id == 1 || id == 2 {
        Ok(Json(Item {
            id,
            name: format!("Item {}", id),
            completed: false,
        }))
    } else {
        Err(AppError::NotFound("Item not found".to_string()))
    }
}

Add route:

.route("/api/items/:id", get(handlers::get_item))

Test:

# Found
curl http://localhost:3001/api/items/1

# Not found (error handling)
curl http://localhost:3001/api/items/999

Step 7: With Database (Optional)

If you scaffolded with --db sqlx-sqlite:

use axum::extract::State;
use sqlx::SqlitePool;

pub async fn list_items(
    State(db): State<SqlitePool>,
) -> Result<Json<Vec<Item>>, AppError> {
    let items = sqlx::query_as::<_, Item>(
        "SELECT id, name, completed FROM items"
    )
    .fetch_all(&db)
    .await?;

    Ok(Json(items))
}

pub async fn create_item(
    State(db): State<SqlitePool>,
    Json(req): Json<CreateItemRequest>,
) -> Result<Json<Item>, AppError> {
    let item = sqlx::query_as::<_, Item>(
        "INSERT INTO items (name, completed) VALUES (?, false) RETURNING id, name, completed"
    )
    .bind(&req.name)
    .fetch_one(&db)
    .await?;

    Ok(Json(item))
}

Update router to pass database:

pub fn build(db: SqlitePool) -> Router {
    Router::new()
        .route("/api/items", get(list_items))
        .route("/api/items", post(create_item))
        .with_state(db)
}

Summary

You now have:

  • ✅ GET /api/items — List all items
  • ✅ POST /api/items — Create an item
  • ✅ GET /api/items/:id — Get one item with error handling
  • ✅ Optional: Database integration

Next Steps