One of the most frustrating parts of backend development is the ‘runtime surprise’—that moment your application crashes because a database column was renamed, or a type mismatch occurred that your compiler didn’t catch. When I first started building with Rust, I wanted that same level of safety for my database layer that I had for my memory management.
That’s where sqlx comes in. In this rust sqlx tutorial postgres guide, I’ll show you how to leverage SQLx to get compile-time verified queries. Unlike traditional ORMs, SQLx allows you to write raw SQL while ensuring that the queries are actually valid against your live database during the build process.
Prerequisites
Before we dive into the code, ensure you have the following installed and running in your environment:
- Rust Toolchain: Latest stable version (via rustup).
- PostgreSQL: A running instance (local or Docker).
- SQLx CLI: Installed via
cargo install sqlx-cli. - A Database: A created database (e.g.,
createdb my_app_db).
Step 1: Project Setup and Dependencies
First, initialize your project and add the necessary crates to your Cargo.toml. I recommend using the tokio runtime as it’s the industry standard for async Rust.
[dependencies]
sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-rustls", "postgres", "macros" ] }
tokio = { version = "1", features = [ "full" ] }
dotenvy = "0.15"
I always use dotenvy to manage my database URLs. Create a .env file in your root directory:
DATABASE_URL=postgres://username:password@localhost:5432/my_app_db
Step 2: Database Migrations
SQLx handles migrations beautifully. Instead of manually running SQL scripts, we use the CLI to maintain version control over our schema.
Run the following command to create your first migration:
sqlx migrate add create_users_table
This creates a migrations/ folder. Open the newly created .sql file and add your schema:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Now, apply the migration to your database:
sqlx migrate run
Step 3: Establishing a Connection Pool
In a production app, you never open a new connection for every request. We use a PgPool, which manages a set of open connections that can be reused across your async tasks.
use sqlx::postgres::PgPoolOptions;
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");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await?;
println!("Connected to Postgres!");
Ok(())
}
Step 4: Performing Type-Safe Queries
This is where the magic happens. SQLx provides two main ways to query: the query function (runtime checked) and the query! macro (compile-time checked). I almost always use the macro for critical paths.
Inserting Data
Here is how I typically handle user registration. Note how the macro validates the column names against the actual database:
async fn create_user(pool: &sqlx::PgPool, username: &str, email: &str) -> Result <!, sqlx::Error> {
sqlx::query!
"INSERT INTO users (username, email) VALUES ($1, $2)",
username, email
.execute(pool)
.await?;
Ok(())
}
Fetching Data
When fetching, SQLx maps the database rows directly into an anonymous struct or a defined one. If you change the table schema and forget to update this code, the project will not compile.
async fn get_user_by_username(pool: &sqlx::PgPool, username: &str) -> Result <User, sqlx::Error> {
let user = sqlx::query_as!
User,
"SELECT id, username, email, created_at FROM users WHERE username = $1",
username
.fetch_one(pool)
.await?;
Ok(user)
}
If you’re debating between different database options for your Rust project, I highly recommend checking out my analysis of SurrealDB vs Postgres in Rust to see which fits your data model better.
Pro Tips for SQLx Mastery
- Offline Mode: Use
sqlx-data.jsonviacargo sqlx prepare. This allows your CI/CD pipeline to compile the code without needing a live database connection. - Transactions: For multi-step operations, use
pool.begin().await?to ensure atomicity. - Custom Types: Use the
#[derive(sqlx::Type)]macro to map Postgres Enums to Rust Enums.
Troubleshooting Common Issues
Error: “database connection not found during compilation”
This usually happens because the DATABASE_URL is missing from your environment when running cargo build. Ensure your .env is loaded or export the variable in your shell.
Error: “mismatched types” in query! macro
Postgres TEXT maps to String, and INT4 maps to i32. If you use INT8, you must use i64 in Rust. Double-check your schema definitions.
What’s Next?
Now that you have a working database layer, the next step is integrating it into a web server. Whether you are building a REST API or a GraphQL server, choosing the right framework is key. I’ve detailed the best Rust web frameworks for 2026 to help you decide between Axum, Actix-web, or Rocket.
Ready to scale? Start by implementing a connection pooling strategy and adding indexing to your most-queried columns.