Introduction: Why Rust and Databases Are a Powerful Combination
Rust is a systems programming language that combines the performance of C/C++ with memory safety. However, for a long time, the ecosystem for working with databases in Rust was considered "immature." Today the situation has changed dramatically: the sqlx library (for PostgreSQL) and the official mongodb driver provide mature, asynchronous, and safe APIs.
In this article, we will practically explore how to connect to PostgreSQL and MongoDB from Rust, perform CRUD operations, and handle errors. All examples will use the tokio async runtime.
1. Project Setup and Dependency Installation
Create a new Cargo project:
cargo new rust_db_examplecd rust_db_exampleAdd the following dependencies to Cargo.toml:
[dependencies]tokio = { version = "1", features = ["full"] }sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres"] }mongodb = "2.8"serde = { version = "1", features = ["derive"] }anyhow = "1"- sqlx — an async driver for PostgreSQL with compile-time query checking.
- mongodb — the official driver from MongoDB Inc.
- serde — for serialization/deserialization of structs.
- anyhow — for convenient error handling.
2. Working with PostgreSQL via sqlx
2.1 Connecting to the Database
For connection, use a connection string of the form postgres://user:password@localhost/dbname. Example:
use sqlx::postgres::PgPool;use anyhow::Result;
#[tokio::main]async fn main() -> Result<()> { let pool = PgPool::connect("postgres://postgres:password@localhost/testdb").await?; println!("Connection to PostgreSQL established!"); Ok(())}2.2 Creating a Table and Inserting Data
Let's define a User struct and perform a migration:
use serde::{Deserialize, Serialize};use sqlx::FromRow;
#[derive(Debug, Serialize, Deserialize, FromRow)]struct User { id: i32, name: String, email: String,}
async fn create_table(pool: &PgPool) -> Result<()> { sqlx::query( "CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE )" ) .execute(pool) .await?; println!("Table users created"); Ok(())}
async fn insert_user(pool: &PgPool, name: &str, email: &str) -> Result<User> { let user = sqlx::query_as::<_, User>( "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email" ) .bind(name) .bind(email) .fetch_one(pool) .await?; Ok(user)}Note $1, $2 — these are parameterized queries that protect against SQL injection.
2.3 Reading and Updating Data
async fn get_user_by_email(pool: &PgPool, email: &str) -> Result<Option<User>> { let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = $1") .bind(email) .fetch_optional(pool) .await?; Ok(user)}
async fn update_user_name(pool: &PgPool, id: i32, new_name: &str) -> Result<()> { sqlx::query("UPDATE users SET name = $1 WHERE id = $2") .bind(new_name) .bind(id) .execute(pool) .await?; println!("User {} updated", id); Ok(())}Using fetch_optional allows handling the case when a record is not found without panicking.