DynamoDB CRUD

Type-safe DynamoDB operations that feel natural in Rust.
No more wrestling with attribute values or expression strings: just clean, composable code that catches errors at compile time.
Why?
Working with DynamoDB in Rust shouldn't mean manually building expression strings or converting between types. This crate gives you:
- ✅ Type safety - Catch errors before they hit production
- ✅ Ergonomic API - Intuitive operations that read like your code
- ✅ Full feature support - Get, Put, Update, Delete, Query, Scan, and Batch operations
- ✅ Zero boilerplate - Let the compiler handle the details
Quick Start
[dependencies]
dynamodb-crud = "0.1"
Basic Operations
use aws_config::load_from_env;
use aws_sdk_dynamodb::Client;
use dynamodb_crud::{common, read, write};
use serde_json::json;
let config = load_from_env().await;
let client = Client::new(&config);
let put_item = write::put_item::PutItem {
item: json!({"id": "1", "name": "John", "age": 30}),
write_args: write::common::WriteArgs {
table_name: "users".to_string(),
..Default::default()
},
};
put_item.send(&client).await?;
let get_item = read::get_item::GetItem {
keys: common::key::Keys {
partition_key: common::key::Key {
name: "id".to_string(),
value: "1".to_string(),
},
..Default::default()
},
single_read_args: read::common::SingleReadArgs {
table_name: "users".to_string(),
..Default::default()
},
..Default::default()
};
get_item.send(&client).await?;
Tracing
Enable the optional tracing feature to emit spans around DynamoDB operations:
[dependencies]
dynamodb-crud = { version = "0.1", features = ["tracing"] }
tracing_subscriber::fmt()
.with_env_filter("dynamodb_crud=debug")
.init();
The Real Advantage: Complex Updates Without Expression Strings
Instead of manually building update expressions like "SET #name = :name, #age = #age + :inc ADD #tags :tags REMOVE #oldAttr" and managing placeholders, just use structured types:
use dynamodb_crud::{common, write};
use serde_json::Value;
let update_item = write::update_item::UpdateItem {
keys: common::key::Keys {
partition_key: common::key::Key {
name: "id".to_string(),
value: Value::String("1".to_string()),
},
..Default::default()
},
update_expression: write::update_item::UpdateExpressionMap::Combined(vec![
write::update_item::UpdateExpressionMap::Set(
write::update_item::SetInputsMap::Leaves(vec![
("name".to_string(), write::update_item::SetInput::Assign(Value::String("Jane".to_string()))),
("age".to_string(), write::update_item::SetInput::Increment(Value::Number(1.into()))),
]),
),
write::update_item::UpdateExpressionMap::Add(
write::update_item::AddOrDeleteInputsMap::Leaves(vec![
("tags".to_string(), Value::Array(vec![
Value::String("new".to_string()),
Value::String("feature".to_string()),
])),
]),
),
write::update_item::UpdateExpressionMap::Remove(
common::selection::SelectionMap::Leaves(vec!["oldAttr".to_string()]),
),
]),
write_args: write::common::WriteArgs {
table_name: "users".to_string(),
..Default::default()
},
};
update_item.send(&client).await?;
The crate automatically:
- ✅ Builds the expression string:
"SET #name = :set0, #age = #age + :set1 ADD #tags :add_or_delete2 REMOVE #oldAttr"
- ✅ Manages expression attribute names:
{"#name": "name", "#age": "age", ...}
- ✅ Manages expression attribute values:
{":set0": "Jane", ":set1": 1, ...}
- ✅ Handles nested paths:
user.profile.email becomes #user.#profile.#email
Query with Filters and Conditions
Query with complex conditions without writing expression strings:
use dynamodb_crud::{common, read};
use serde_json::Value;
let query = read::query::Query {
partition_key: common::key::Key {
name: "userId".to_string(),
value: Value::String("user123".to_string()),
},
sort_key_condition: Some(common::condition::KeyCondition {
name: "timestamp".to_string(),
condition: common::condition::Condition::GreaterThan(Value::Number(1000.into())),
}),
multiple_read_args: read::common::MultipleReadArgs {
table_name: "events".to_string(),
condition: Some(common::condition::ConditionMap::Leaves(
common::condition::LogicalOperator::And,
vec![
common::condition::KeyCondition {
name: "status".to_string(),
condition: common::condition::Condition::Equals(Value::String("active".to_string())),
},
common::condition::KeyCondition {
name: "priority".to_string(),
condition: common::condition::Condition::GreaterThan(Value::Number(5.into())),
},
],
)),
selection: Some(common::selection::SelectionMap::Leaves(vec![
"eventId".to_string(),
"message".to_string(),
])),
..Default::default()
},
..Default::default()
};
query.send(&client).await?;
Conditional Writes
Add conditions to prevent overwrites or ensure data integrity:
use dynamodb_crud::{common, write};
use serde_json::Value;
let update_item = write::update_item::UpdateItem {
keys: common::key::Keys {
partition_key: common::key::Key {
name: "id".to_string(),
value: Value::String("1".to_string()),
},
..Default::default()
},
update_expression: write::update_item::UpdateExpressionMap::Set(
write::update_item::SetInputsMap::Leaves(vec![
("status".to_string(), write::update_item::SetInput::Assign(Value::String("retired".to_string()))),
]),
),
write_args: write::common::WriteArgs {
table_name: "users".to_string(),
condition: Some(common::condition::ConditionMap::Leaves(
common::condition::LogicalOperator::And,
vec![
common::condition::KeyCondition {
name: "age".to_string(),
condition: common::condition::Condition::GreaterThanOrEqual(Value::Number(65.into())),
},
common::condition::KeyCondition {
name: "status".to_string(),
condition: common::condition::Condition::NotEqual(Value::String("retired".to_string())),
},
],
)),
..Default::default()
},
};
update_item.send(&client).await?;
Documentation