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:

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
Terminal window showing SQLx CLI migration commands and success messages
Terminal window showing SQLx CLI migration commands and success messages

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)
}
VS Code error showing compile-time SQL validation failure
VS Code error showing compile-time SQL validation failure

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

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.