Introduction
As a mostly self‑learned developer, I took the classic web‑developer route and started my software journey with JavaScript. That eventually led to building some full‑stack applications and seeing real progress. However, I noticed that most of the people I’m competing with aren’t one‑trick ponies — they also know system languages like C and C++. Naturally, I had to be different… so I chose Rust.
I knew there was no way I could learn Rust without getting my feet wet and working on something meaningful. Initially, I wanted to build yet another full‑stack web app using Leptos — and let me tell you, I gave up on that project at least 100 times before actually giving up. Every time I fixed one part, another part broke. It was like playing whack‑a‑mole with the compiler. Eventually, I realized it just wasn’t the right time to build another website.
One of the main pain points was setting up MongoDB. It gave me enough trouble that I started thinking: what if I built something like Mongoose, but in Rust? That idea became OxiMod.
What Is OxiMod?
OxiMod is a schema-based Object-Document Mapper (ODM) for MongoDB written in Rust. It is built for developers who prefer defining models using Rust's type system and want to avoid repetitive boilerplate typically associated with manual query logic.
It uses a single derive macro and struct-level attributes to generate CRUD methods and validate field-level logic. The crate integrates with the official MongoDB driver and supports any async runtime. Its main goal is to provide an idiomatic way to model and interact with MongoDB collections.
We will now take a look at the current version of OxiMod (v0.1.9) at the time of writing this article.
Setting Up the MongoDB Client
Before using any model, you need to initialize a global MongoDB client using a URI:
pub async fn init() {
dotenv::dotenv().ok();
let uri = std::env::var("MONGODB_URI").expect("Missing MONGODB_URI");
set_global_client(uri).await.unwrap();
}
Defining a Model
#[derive(Debug, Serialize, Deserialize, Model)]
#[db("my_app_db")]
#[collection("users")]
struct User {
#[serde(skip_serializing_if = "Option::is_none")]
_id: Option<ObjectId>,
#[index(unique, name = "email_idx", order = -1)]
email: String,
#[validate(min_length = 3)]
name: String,
#[validate(non_negative)]
age: i32,
#[default(false)]
active: bool,
}
This struct will automatically implement all the required traits for interacting with MongoDB. Indexes, validation rules, and default values can be declared inline using field-level attributes.
If you need access to the connection elsewhere in your application — for example, to manually create an index that isn't yet supported by OxiMod — you can use something like:
let coll = User::get_collection()?; // given User has derived Model
This provides direct access to the underlying Collection<Document>
.
Builder API
OxiMod provides a new()
method for models, which returns a builder with setter methods for each field. This builder pattern improves readability and helps enforce field completeness at compile time.
let user = User::new()
.name("Alice".into())
.age(30)
.active(true);
Documents with an _id
field automatically receive a default builder setter named id
. However, in rare cases this may clash with another struct field named id
, since the new
and default
builders generate setters based on field names. To resolve this, you can use #[document_id_setter_ident("with_id")]
to customize the setter name for greater flexibility. You can find an example of this below:
#[derive(Debug, Serialize, Deserialize, Model)]
#[db("my_app_db")]
#[collection("users")]
#[document_id_setter_ident("my_custom_id_setter")]
struct User {
#[serde(skip_serializing_if = "Option::is_none")]
_id: Option<ObjectId>,
#[index(unique, name = "email_idx", order = -1)]
email: String,
#[validate(min_length = 3)]
name: String,
#[validate(non_negative)]
age: i32,
#[default(false)]
active: bool,
}
User::default()
.my_custom_id_setter(ObjectId::new())
.id("3894HR934HR00NJ23R324R".to_string())
.name("User1".to_string())
.age(42)
.active(true)
.save().await?;
CRUD Operations
Each model implements common MongoDB operations using async functions. These include save
, find
, update
, and delete
methods.
let id = user.save().await?;
let fetched = User::find_by_id(&id).await?;
User::update_one(doc! { "_id": &id }, doc! { "$set": { "active": true } }).await?;
User::delete_by_id(id).await?;
All operations return result types using thiserror
for improved error reporting.
Validation and Indexing
OxiMod provides attribute-based validation on fields. You can specify rules such as minimum string length, numeric bounds, or email format directly on the model:
#[validate(min_length = 3)]
name: String,
#[validate(positive)]
age: i32,
Field-level indexing is also supported:
#[index(unique, sparse)]
email: String,
Supported Options:
unique
: Ensures values in this field are unique.sparse
: Indexes only documents that contain the field.name = "...""
: Custom name for the index.background
: Builds index in the background without locking the database.order = 1 | -1
: Index sort order (1 = ascending, -1 = descending).expire_after_secs = ...
: Time-to-live for the index in seconds.
Further support for indexes is planned.
Custom Logic Using Rust's impl
OxiMod does not implement built-in macro-based hooks such as #[pre_save]
because they add hidden costs and limitations (e.g., requiring the Copy
trait or breaking compatibility with fluent builders). Instead, users are encouraged to implement their own logic explicitly.
#[derive(Debug, Serialize, Deserialize, Model)]
#[db("hook_example_db")]
#[collection("logs")]
struct Log {
#[serde(skip_serializing_if = "Option::is_none")]
_id: Option<ObjectId>,
message: String,
timestamp: i64,
}
// Pre-save hook implementation
impl Log {
fn print_message(self) -> Self {
println!("📋 Log message: {}", self.message);
self
}
}
let log_id = Log::default()
.message("System started".to_string())
.timestamp(DateTime::now().timestamp_millis())
.print_message()
.save().await?;
Roadmap
The following improvements are planned:
- Additional test coverage, particularly for multi-threaded scenarios
- Expanded validation system (e.g., URL, IP validation, and cross-field checks)
- Additional index support, including partial and text indexes
- Performance improvements such as reducing memory allocations and binary size
Conclusion
OxiMod is an Object-Document Mapper for Rust designed to offer schema modeling, field-level validation, fluent builders, and clear error handling — all while keeping the codebase minimal and extensible. It is suitable for developers looking for a structured yet flexible approach to working with MongoDB in asynchronous Rust environments.
The project is evolving, and contributions and feedback are warmly welcome.
Resources
- 📦 Crate
- 💻 Repository