When we build modern web applications, connecting our backend to a database is one of the most important tasks, where we can store user profiles, product data, or analytics logs, so our application communicates with the database directly impacts its speed, reliability, and scalability.
Rust is one of the best languages for database interactions because it combines performance, safety, and developer-friendly features in a single package. Here, you can see its great features:
1) High Performance: Database operations can become important in many applications, especially when handling a large volume of queries or working with millions of records. Rust solves this problem because it compiles to highly optimized machine code and runs almost as fast as C or C++.
This means your API can perform searching, filtering, and aggregating results far more quickly than many other languages.
2) Memory Safety: Traditional languages often risk database-related bugs like dangling pointers, accessing invalid memory, and memory leaks when queries fail or connections are not closed properly.
Rust’s ownership and borrowing system eliminates these issues at compile time, meaning such bugs are caught before your code even runs.
3) Predictable, Maintainable, and Secure Code: Because Rust catches most errors during compilation, database code becomes much more predictable and stable. It’s easier to maintain, and the chances of runtime crashes or security vulnerabilities are drastically reduced.
Popular Database Libraries in Rust
Below are the most popular and widely used libraries that make working with databases in Rust both efficient and developer-friendly:
1. Diesel – Type-Safe ORM for Maximum Reliability
Diesel is one of the most mature and powerful database libraries in the Rust ecosystem. It’s an ORM (Object-Relational Mapper) that allows you to work with databases in a Rust-like, type-safe way, which means your queries are checked at compile time instead of runtime.
- Productivity Boost: Diesel uses macros to simplify query writing, making code both readable and safe.
- Multiple Database Support: Works with PostgreSQL, MySQL, and SQLite out of the box.
2. SQLx – Async and Flexible SQL Toolkit
If you prefer writing raw SQL queries but still want the power of Rust’s safety features, SQLx is the perfect choice. It’s a fully asynchronous, runtime-agnostic SQL library that works without an ORM
- Flexible and Lightweight: No heavy abstractions, you interact directly with your SQL statements.
- Multi-Database Compatibility: It works seamlessly with PostgreSQL, MySQL, and SQLite.
It is best for developers who want maximum control over their SQL and need asynchronous query execution, and perfect for modern web backends, microservices, and data-intensive APIs.
3. SeaORM – Easy, High-Level ORM
SeaORM is a relatively newer but increasingly popular ORM that focuses on developer experience and simplicity. It’s designed for those who want to work at a higher level of abstraction without worrying too much about raw SQL.
- Migration Tools: Comes with built-in support for schema migrations and model generation.
- Relation Handling: Makes working with relationships (like one-to-many or many-to-many) straightforward and type-safe.
Setting Up a Rust Project for Database Interaction
Let’s create a Rust project to work with databases.
Step 1: Create a New Project
First, open your terminal and create a new Rust project with the following commands:
cargo new rust-db-connection
cd rust-db-connection
This will create a new folder named rust-db-connection with a basic project structure.
Step 2: Add Database Libraries
To work with databases, we need a library that facilitates easy connections and queries. One of the most popular choices is SQLx, an asynchronous and type-safe SQL library.
Open your Cargo.toml file and add the following dependencies:
[dependencies]
sqlx = { version = "0.6", features = ["postgres", "runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
dotenvy = "0.15"
- sqlx – Provides async database access and compile-time query checking.
- tokio – The asynchronous runtime required by SQLx.
- dotenvy – Loads environment variables from a .env file.
Then run:
cargo build
This will download and compile all the required libraries.
2. Configure Your Database Connection
Before connecting, we must store our database credentials securely. Instead of hardcoding them, we use a .env file to keep sensitive details out of our code.
Create a new file called .env in the root of your project and add the following line (replace values with your actual database details):
DATABASE_URL=postgres://username:password@localhost/my_database
- username – Your PostgreSQL username
- password – Your PostgreSQL password
- my_database – The name of the database you want to connect to
Connect to PostgreSQL Database (with SQLx)
Now we’re ready to write the Rust code that connects to the database. Here’s a simple and unique example:
use sqlx::postgres::PgPoolOptions;
use dotenvy::dotenv;
use std::env;
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
// Load variables from .env
dotenv().ok();
// Read DATABASE_URL from environment
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL is not set in .env file");
// Create a connection pool
let pool = PgPoolOptions::new()
.max_connections(5) // limit concurrent connections
.connect(&database_url)
.await?;
println!("Successfully connected to the database!");
// You can now use `pool` to run queries...
Ok(())
}
Explanation:
- dotenv().ok(); loads the environment variables from the .env file, allowing us to safely use them in the code.
- env::var(“DATABASE_URL”) Reads the database connection string stored in the .env file.
Run the application:
cargo run
If everything is configured correctly, you should see:
Successfully connected to the database!
3. Performing CRUD Operations in Rust (Using SQLx)
Once you’ve successfully connected your Rust project to a database, the next step is to perform CRUD operations: Create, Read, Update, and Delete.
These are the most common tasks in any web application that deals with data.
Example: CRUD with SQLx
Create a Database Table
Before writing any Rust code, we need a table to store data. Connect to your PostgreSQL database (using psql or any GUI tool like pgAdmin) and create a table named users:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
Insert Data (CREATE)
Let’s start by adding a new user to the database.
use sqlx::postgres::PgPoolOptions;
use sqlx::query;
use dotenvy::dotenv;
use std::env;
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// Create a connection pool
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await?;
// Insert a new user into the table
query("INSERT INTO users (name, email) VALUES ($1, $2)")
.bind("Alice Johnson")
.bind("alice@example.com")
.execute(&pool)
.await?;
println!("New user added successfully!");
Ok(())
}
Explanation:
- .execute() – Executes the query without expecting any returned rows.
- query() – Runs a raw SQL query.
- .bind() – Safely inserts dynamic values into the query (prevents SQL injection).
Fetch Data (READ)
Now let’s read data from the users table and print it.
use sqlx::{postgres::PgPoolOptions, FromRow, query_as};
use dotenvy::dotenv;
use std::env;
#[derive(FromRow, Debug)]
struct User {
id: i32,
name: String,
email: String,
}
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await?;
// Fetch all users
let users: Vec<User> = query_as::<_, User>("SELECT * FROM users")
.fetch_all(&pool)
.await?;
println!("All users:");
for user in users {
println!("ID: {}, Name: {}, Email: {}", user.id, user.name, user.email);
}
Ok(())
}
Update Data (UPDATE)
We can also update an existing user’s information. For example, changing a user’s email:
use sqlx::query;
async fn update_user_email(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
query("UPDATE users SET email = $1 WHERE name = $2")
.bind("alice_new@example.com")
.bind("Alice Johnson")
.execute(pool)
.await?;
println!("User email updated successfully!");
Ok(())
}
Delete Data (DELETE)
Finally, let’s remove a user from the database.
use sqlx::query;
async fn delete_user(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
query("DELETE FROM users WHERE name = $1")
.bind("Alice Johnson")
.execute(pool)
.await?;
println!("User deleted successfully!");
Ok(())
}
- DELETE removes data from the table.
- Again, the WHERE clause is important, without it, all records would be deleted.
4. Using Diesel ORM
While writing raw SQL queries with libraries like SQLx is powerful, it can become repetitive and error-prone, especially as your project grows.
Diesel is a high-level ORM (Object-Relational Mapper) for Rust. It allows you to interact with databases using Rust’s type system instead of writing raw SQL, which improves safety and reduces runtime errors.
Example: Insert Data with Diesel
Before using Diesel, install the Diesel CLI tool. This helps with tasks like creating database migrations and setting up the schema:
cargo install diesel_cli --no-default-features --features postgres
Then initialize Diesel in your project:
diesel setup
Make sure your .env file has the database URL:
DATABASE_URL=postgres://username:password@localhost/database_name
Define a Database Schema
Diesel generates a schema.rs file based on your database structure. But first, you should create a migration to define your users table.
Run:
diesel migration generate create_users
This creates a new migration folder. Inside the generated up.sql file, define the table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL
);
Then run the migration:
diesel migration run
This will create the table and update the schema.rs file automatically.
In Diesel, we represent each table as a Rust struct. We’ll define a User struct to represent a row in the users table.
use diesel::prelude::*;
use diesel::Queryable;
#[derive(Queryable, Debug)]
struct User {
id: i32,
name: String,
email: String,
}
- #[derive(Queryable)] tells Diesel that this struct maps to a database row.
- Each field matches a column in the users table.
Insert Data into the Database (CREATE)
To insert new records, we define another struct that represents the new data (without the ID field since it’s generated automatically).
use diesel::prelude::*;
use diesel::insert_into;
use dotenvy::dotenv;
use std::env;
mod schema {
diesel::table! {
users (id) {
id -> Int4,
name -> Varchar,
email -> Varchar,
}
}
}
use self::schema::users;
#[derive(Insertable)]
#[diesel(table_name = users)]
struct NewUser<'a> {
name: &'a str,
email: &'a str,
}
fn main() {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let connection = PgConnection::establish(&database_url)
.expect("Failed to connect to the database");
let new_user = NewUser {
name: "Alice Johnson",
email: "alice@example.com",
};
// Insert the new record into the database
insert_into(users::table)
.values(&new_user)
.execute(&connection)
.expect("Failed to insert user");
println!("New user added successfully!");
}
- Insertable – Marks a struct that can be inserted into a database table.
- insert_into() – Tells Diesel which table to insert data into.