Building a High-Performance CRUD REST API in Rust with Axem, SurrealDB, Tokio, and Tower
1. Why Do We Need This? (Problem Statement & Status Quo)
Modern backend engineering often requires balancing extreme execution speed, memory safety, and an intuitive developer experience (DX). Traditional relational or document databases often come with heavy setup overhead, complex connection pooling configuration, or cumbersome query builders.
When building a high-throughput RESTful service in Rust, developers traditionally pair a web framework like Axum with complex database drivers for PostgreSQL, MySQL, or MongoDB. While powerful, this boilerplate-heavy approach can obscure core business logic. This project explores an alternative stack: pairing Rust, the Axum web framework, the Tokio asynchronous runtime, the Tower middleware ecosystem, and SurrealDB—an innovative, cloud-native distributed database known for its exceptionally smooth developer experience and lightning-fast local setup.
2. Core Mental Model: The Clean Architecture Blueprint
To maintain code readability and prevent architectural spaghetti as a project scales, this application adopts a clean, modular directory structure. Separation of concerns is enforced by dividing code into distinct functional layers:
Real-Life Analogy: The Restaurant Kitchen Workflow
Think of this clean architecture like a well-run restaurant:
main.rs&lib.rs(The Front Door & Host Stand): Welcomes guests (incoming HTTP requests), configures security policies (CORS), and assigns tables (starts the Tokio TCP listener on port 8080).api/(The Waitstaff): Takes orders from customers, validates basic format, and routes requests to the correct kitchen station.application/(The Line Cooks - Commands & Queries): Divides operations strictly between writing/modifying food (Commands like Create, Update, Delete) and reading the menu (Queries like Get All, Get By ID).infrastructure/(The Pantry & Storage Room): Directly interacts with the raw storage units (SurrealDB) to fetch or persist ingredients.
3. Project Initialization and Entrypoint (main.rs)
The application entrypoint sets up the Axum router, injects middleware (such as CORS via Tower), and boots the Tokio asynchronous runtime to bind the server to port 8080.
// main.rs
use axum::{
routing::get,
Router,
};
use tower_http::cors::{CorsLayer, Any};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
// Configure CORS layer allowing localhost frontend origins and standard methods
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::PATCH, axum::http::Method::DELETE])
.allow_headers(Any);
// Build the router and attach routes and middleware
let app = Router::new()
.merge(surreal_api::api::router::app_router())
.layer(cors);
// Bind TCP listener using Tokio on port 8080
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
println!("Server started successfully on port 8080");
// Serve the application using Axum
axum::serve(listener, app).await.unwrap();
}
4. API Routing Architecture (api/router.rs)
The API module maps incoming URL paths to specific handler functions. It cleanly separates health checks, general resource endpoints, and parameterized routes.
// api/router.rs
use axum::{
routing::{get, post, patch, delete},
Router,
};
use super::mod::*;
pub fn app_router() -> Router {
Router::new()
.route("/api/health", get(health_checker_handler))
.route("/api/todos", get(get_all_todos_query).post(create_todo_command))
.route("/api/todos/:id", get(get_todo_by_id_query).patch(update_todo_command).delete(delete_todo_command))
}
Route Mapping Summary
5. Domain Models (domain/models.rs)
The domain layer defines the core data structures of the application, independent of any web framework or database specifics. Here, our primary entity is the Todo struct, which includes unique identification, content descriptions, completion status, and precise timestamps managed via Chrono.
// domain/models.rs
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Local};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Todo {
pub id: Option<String>,
pub title: String,
pub content: String,
pub completed: bool,
pub created_at: DateTime<Local>,
pub updated_at: DateTime<Local>,
}
6. Application Layer: Commands vs. Queries
A key architectural pattern in this project is separating operations into two distinct paradigms:
- Commands: Actions that mutate state or modify data in the database (Create, Update, Delete). These require strict validation and error handling because they change system state.
- Queries: Read-only operations that fetch data without altering state (Get All, Get By ID).
Command Implementation: Creating a Todo
When executing a CreateTodo command, the system checks whether a to-do with the same title already exists to enforce uniqueness. If it does not exist, timestamps are generated, completion is defaulted to false, and the repository persists the record.
// application/commands.rs ( conceptual workflow )
pub async fn create_todo_command(
State(db): State<SurrealContext>,
Json(payload): Json<CreateTodoRequest>,
) -> Result<impl IntoResponse, StatusCode> {
let repo = TodoRepository::new();
// Check if a todo with this title already exists
if repo.get_by_title(&payload.title).await.is_ok() {
return Err(StatusCode::BAD_REQUEST);
}
let now = Local::now();
let new_todo = Todo {
id: None,
title: payload.title,
content: payload.content,
completed: false,
created_at: now,
updated_at: now,
};
match repo.create(new_todo).await {
Ok(created) => Ok((StatusCode::CREATED, Json(created))),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
7. Infrastructure Layer & SurrealDB Integration (infrastructure/repository.rs)
SurrealDB provides an exceptional developer experience because it can be initialized instantly in memory or on disk with a single command. The infrastructure layer connects to SurrealDB and implements repository methods for database interaction.
Starting SurrealDB Locally
To run SurrealDB locally for development, execute a single command in your terminal:
surreal start --user root --pass root --ns test --db test memory
Repository Data Operations
The repository encapsulates raw database queries, interacting directly with SurrealDB's data primitives.
// infrastructure/repository.rs
pub struct TodoRepository {
table: String,
}
impl TodoRepository {
pub fn new() -> Self {
Self {
table: "todo".to_string(),
}
}
pub async fn get_all(&self) -> Result<Vec<Todo>, surrealdb::Error> {
// Select all records from the todo table
let mut response = DB.select(&self.table).await?;
let todos: Vec<Todo> = response.take(0)?;
Ok(todos)
}
pub async fn get_by_id(&self, id: &str) -> Result<Todo, surrealdb::Error> {
let record: Option<Todo> = DB.select((&self.table, id)).await?;
match record {
Some(todo) => Ok(todo),
None => Err(surrealdb::Error::Api("Todo not found".into())),
}
}
pub async fn create(&self, todo: Todo) -> Result<Todo, surrealdb::Error> {
let created: Option<Todo> = DB.create(&self.table).content(todo).await?;
Ok(created.unwrap())
}
}
8. Complete Flow Walkthrough: Creating a Todo
When a client sends an HTTP POST request to create a to-do item, the request traverses the system layers as follows:
Client (Postman / Frontend)
|
| HTTP POST /api/todos with JSON payload
v
Axum Router (api/router.rs)
|
| matches route and invokes
v
Create Todo Command (application/commands.rs)
|
| checks uniqueness via repo.get_by_title()
v
Todo Repository (infrastructure/repository.rs)
|
| executes DB.create() query
v
SurrealDB Engine (Running on Localhost)
|
| persists record and returns stored model
v
Axum returns HTTP 201 Created with JSON Response to Client
9. Summary and Key Takeaways
- Rust + Axum Ecosystem: Combining Rust with Axum and Tokio provides unmatched execution speed, memory safety, and robust concurrency for building microservices.
- SurrealDB Developer Experience: SurrealDB offers a streamlined, cloud-native database experience that can be booted instantly with a single command (
surreal start ... memory). - Clean Modular Architecture: Structuring the project into distinct layers (
main.rs,api,application,domain, andinfrastructure) ensures long-term maintainability and separation of concerns. - Command and Query Segregation: Separating state-mutating actions (Commands) from read-only operations (Queries) makes critical business logic transparent and easier to debug.
- Tower Middleware Integration: Easily attach production-grade middleware such as CORS policies using Tower layers directly into the Axum router pipeline.
- Open Source Availability: The complete reference project is fully open-sourced on GitHub (
AkilCharm9 / rust-surreal-db-api), offering developers a plug-and-play template for modern Rust backend development.