This episode discusses data management in Rust: serialization and deserialization with serde for the JSON and TOML formats, file and network stream I/O, async I/O with tokio, as well as database integration with sqlx, diesel, or sea-orm.

Modern applications rarely contain static data inside the code: you read configuration, store data, call APIs, and write logs. All of it involves serialization — converting data into a format that can be sent and stored — as well as I/O with files, networks, and databases.
Episode 8 breaks down all three: serde as the de facto serialization standard, file and async I/O with tokio, and database integration with sqlx. After this episode, you will be able to build a real data layer.
serde is a serialization framework with a rich format ecosystem: JSON, TOML, YAML, and many more. Everything is driven by the Serialize and Deserialize derive macros.
cargo add serde --features derive
cargo add serde_jsoncargo add serde --features derive enables the macros. serde_json provides the JSON format. A data structure simply needs a derive annotation:
cat > src/main.rs <<'EOF'
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Server {
nama: String,
port: u16,
aktif: bool,
}
fn main() {
let server = Server {
nama: String::from("api"),
port: 8080,
aktif: true,
};
let json = serde_json::to_string(&server).unwrap();
println!("{}", json);
let kembali: Server = serde_json::from_str(&json).unwrap();
println!("{}:{}", kembali.nama, kembali.port);
}
EOF
cargo runserde_json::to_string turns a struct into JSON, and from_str reverses the process. Because validation happens through types, mismatched data is rejected at deserialization time — not later when it is used.
The same principle applies to other formats. toml::from_str parses TOML text into a struct using the same derive pattern:
cat > src/main.rs <<'EOF'
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
host: String,
port: u16,
}
fn main() {
let sumber = r#"
host = "127.0.0.1"
port = 5432
"#;
let config: Config = toml::from_str(sumber).unwrap();
println!("{}:{}", config.host, config.port);
}
EOF
cargo runThe same #[derive(Deserialize)] works for JSON, TOML, and YAML — that is the power of serde: one type definition, many formats.
The standard library provides synchronous file I/O in std::fs:
cat > src/main.rs <<'EOF'
use std::fs;
fn main() {
fs::write("catatan.txt", "belajar rust\n").unwrap();
let isi = fs::read_to_string("catatan.txt").unwrap();
println!("{}", isi.trim());
fs::remove_file("catatan.txt").unwrap();
}
EOF
cargo runfs::write, fs::read_to_string, and fs::remove_file handle the common cases. For more advanced needs, File, Read, and Write give full control. All of them return a Result, so errors can be propagated with ?.
Slow I/O operations should not block a thread. tokio is the most popular async runtime: it provides an executor and non-blocking I/O.
cat > src/main.rs <<'EOF'
use tokio::fs;
#[tokio::main]
async fn main() {
let isi = fs::read_to_string("Cargo.toml").await.unwrap();
println!("baris: {}", isi.lines().count());
}
EOF
cargo run#[tokio::main] turns main into an async entry point. .await suspends execution without blocking the thread — the same thread can serve many operations concurrently. This pattern will be used fully in episodes 10 and 12.
sqlx checks SQL queries at compile time, so typos in SQL are caught before runtime:
cat > src/main.rs <<'EOF'
use anyhow::Result;
use sqlx::postgres::PgPoolOptions;
#[derive(sqlx::FromRow)]
struct Pengguna {
id: i32,
nama: String,
}
#[tokio::main]
async fn main() -> Result<()> {
let pool = PgPoolOptions::new()
.max_connections(5)
.connect("postgres://user:pass@localhost/db")
.await?;
let pengguna: Vec<Pengguna> = sqlx::query_as(
"SELECT id, nama FROM pengguna WHERE aktif = true",
)
.fetch_all(&pool)
.await?;
println!("total: {}", pengguna.len());
Ok(())
}
EOF
cargo runPgPoolOptions creates a connection pool shared between tasks. query_as maps rows into the Pengguna struct. With DATABASE_URL and the postgres feature, the query! macro can validate queries against the database schema directly at compile time.
Alternatives exist: diesel is a synchronous, type-safe ORM, while sea-orm is an async ORM built on top of sqlx. The choice: sqlx for full control over SQL, diesel for a mature ORM with strong migration support, sea-orm for the async ecosystem and active-record models. All support schema migrations — a theme we continue in episode 9.
Key takeaways:
Serialize and Deserialize derives is the serialization standard.std::fs for synchronous I/O; tokio for non-blocking async I/O.In the next episode 9 we will discuss schema evolution, config, and environment — application configuration with config, dotenv, or figment, data versioning and type compatibility, as well as the practice of separating config per environment for dev, staging, and production.