Building a CRUD (Create, Read, Update, Delete) server in Rust is an excellent way to learn both Rust’s powerful type system and modern web development patterns. In this tutorial, we’ll create a robust REST API using Axum, one of Rust’s most popular web frameworks.
What We’ll Build
We’ll create a simple task management API with the following endpoints:
GET /tasks
- List all tasksPOST /tasks
- Create a new taskGET /tasks/{id}
- Get a specific taskPUT /tasks/{id}
- Update a taskDELETE /tasks/{id}
- Delete a task
Prerequisites
- Rust installed (rustup.rs)
- Basic understanding of Rust syntax
- Familiarity with REST APIs
Setting Up the Project
First, create a new Rust project:
cargo new rust-crud-server
cd rust-crud-server
Add the necessary dependencies to your Cargo.toml
:
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.0", features = ["v4", "serde"] }
Defining Our Data Model
Create a Task
struct that represents our data:
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: Uuid,
pub title: String,
pub description: Option<String>,
pub completed: bool,
}
#[derive(Debug, Deserialize)]
pub struct CreateTask {
pub title: String,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateTask {
pub title: Option<String>,
pub description: Option<String>,
pub completed: Option<bool>,
}
Creating the In-Memory Store
For simplicity, we’ll use an in-memory store. In production, you’d use a database:
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub type TaskStore = Arc<RwLock<HashMap<Uuid, Task>>>;
pub fn create_store() -> TaskStore {
Arc::new(RwLock::new(HashMap::new()))
}
Implementing CRUD Handlers
Now let’s implement our CRUD operations:
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
routing::{delete, get, post, put},
Router,
};
// Create a new task
async fn create_task(
State(store): State<TaskStore>,
Json(payload): Json<CreateTask>,
) -> Result<Json<Task>, StatusCode> {
let task = Task {
id: Uuid::new_v4(),
title: payload.title,
description: payload.description,
completed: false,
};
let mut tasks = store.write().await;
tasks.insert(task.id, task.clone());
Ok(Json(task))
}
// Get all tasks
async fn get_tasks(State(store): State<TaskStore>) -> Json<Vec<Task>> {
let tasks = store.read().await;
let task_list: Vec<Task> = tasks.values().cloned().collect();
Json(task_list)
}
// Get a specific task
async fn get_task(
Path(id): Path<Uuid>,
State(store): State<TaskStore>,
) -> Result<Json<Task>, StatusCode> {
let tasks = store.read().await;
match tasks.get(&id) {
Some(task) => Ok(Json(task.clone())),
None => Err(StatusCode::NOT_FOUND),
}
}
// Update a task
async fn update_task(
Path(id): Path<Uuid>,
State(store): State<TaskStore>,
Json(payload): Json<UpdateTask>,
) -> Result<Json<Task>, StatusCode> {
let mut tasks = store.write().await;
match tasks.get_mut(&id) {
Some(task) => {
if let Some(title) = payload.title {
task.title = title;
}
if let Some(description) = payload.description {
task.description = Some(description);
}
if let Some(completed) = payload.completed {
task.completed = completed;
}
Ok(Json(task.clone()))
}
None => Err(StatusCode::NOT_FOUND),
}
}
// Delete a task
async fn delete_task(
Path(id): Path<Uuid>,
State(store): State<TaskStore>,
) -> StatusCode {
let mut tasks = store.write().await;
match tasks.remove(&id) {
Some(_) => StatusCode::NO_CONTENT,
None => StatusCode::NOT_FOUND,
}
}
Setting Up the Router and Server
Finally, let’s wire everything together:
#[tokio::main]
async fn main() {
let store = create_store();
let app = Router::new()
.route("/tasks", get(get_tasks).post(create_task))
.route("/tasks/:id", get(get_task).put(update_task).delete(delete_task))
.with_state(store);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
Testing Your API
Start your server:
cargo run
Test with curl:
# Create a task
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn Rust", "description": "Build a CRUD API"}'
# Get all tasks
curl http://localhost:3000/tasks
# Update a task (replace {id} with actual UUID)
curl -X PUT http://localhost:3000/tasks/{id} \
-H "Content-Type: application/json" \
-d '{"completed": true}'
Next Steps
This basic CRUD server can be enhanced with:
- Database integration (PostgreSQL, SQLite)
- Authentication and authorization
- Input validation
- Error handling middleware
- Logging
- Testing
Conclusion
Rust’s type system and performance make it an excellent choice for building robust web APIs. The combination of Axum’s ergonomic API and Rust’s safety guarantees provides a solid foundation for scalable backend services.
The complete code for this tutorial is available as a reference for building more complex applications.