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
- Your First Component — Call this endpoint from React
- API Patterns — Pagination, validation, middleware
- Full-Stack Integration — Connect API to frontend