Working with Databases in Rust: PostgreSQL and MongoDB in Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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_example

Add 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.



3. Working with MongoDB via the Official Driver

3.1 Connecting and Selecting a Database

Blogs

Book Recommendations