SQLx Rust: PostgreSQL Assíncrono e Seguro | Rust Brasil

SQLx em Rust: guia completo de PostgreSQL, MySQL e SQLite assíncronos com queries verificadas em compile-time, pools de conexão, migrations e Axum.

Introdução

O SQLx é a biblioteca Rust mais usada para acessar bancos de dados relacionais de forma assíncrona com queries SQL verificadas em tempo de compilação. Com SQLx Rust você escreve SQL puro para PostgreSQL, MySQL, MariaDB ou SQLite, e o compilador valida colunas, tipos e tabelas antes de o código rodar – sem abrir mão de pools de conexão, migrations e integração com Axum e Tokio. Em vez de descobrir erros de SQL – colunas inexistentes, tipos incompatíveis, tabelas erradas – só em produção, você os detecta em compile-time.

Diferente de ORMs tradicionais como Diesel, o SQLx não abstrai o SQL. Você escreve SQL puro e o SQLx verifica a correção e mapeia os resultados para structs Rust automaticamente. Essa abordagem combina a flexibilidade do SQL puro com a segurança de tipos do Rust.

Por que usar o SQLx?

  • Queries verificadas em compile-time: erros de SQL detectados antes de executar
  • SQL puro: sem DSL ou linguagem intermediária
  • 100% assíncrono: construído sobre Tokio, async-std ou actix
  • Connection pooling: pool de conexões embutido e configurável
  • Migrations: sistema de migrations integrado
  • Multi-banco: PostgreSQL, MySQL, MariaDB, SQLite
  • Streaming: resultados como streams assíncronos
  • FromRow: mapeamento automático de linhas para structs

SQLx Rust em 2026: onde ele encaixa

Quem pesquisa SQLx Rust normalmente já decidiu que quer construir backend real, não apenas exemplos de sintaxe. A pergunta prática é como ligar uma API Axum, um worker Tokio ou um serviço interno a PostgreSQL sem abrir mão de SQL explícito, segurança de tipos e operação previsível. É exatamente nesse espaço que SQLx se destaca.

Em aplicações brasileiras e remotas, a combinação mais comum é Rust, Axum, SQLx e PostgreSQL. Axum cuida da borda HTTP, SQLx cuida de queries, pools, migrations e transações, e Tracing ajuda a observar latência e erros. Essa stack aparece em produtos financeiros, plataformas B2B, ferramentas internas, APIs de alta concorrência, sistemas de dados e serviços que precisam gastar menos memória por instância.

SQLx também é uma ponte natural para quem vem de Go, Python, Node ou Java e gosta de SQL direto. Em vez de aprender uma DSL de ORM antes de entregar valor, você continua escrevendo SELECT, INSERT, UPDATE, joins, CTEs e transações, mas deixa o compilador validar tipos e colunas quando usa as macros query! e query_as!. Para comparar abordagens, veja também Diesel ORM Rust e o artigo Diesel vs SQLx.

Para carreira, SQLx conversa diretamente com vagas Rust que citam backend, PostgreSQL, APIs, sistemas distribuídos, fintech, observabilidade e cloud. Muitas empresas que usam Rust não anunciam “SQLx” no título, mas descrevem serviços que dependem de banco relacional, migrations seguras, pools bem configurados e domínio de falhas de produção.

Instalação

Adicione o SQLx ao seu Cargo.toml:

[dependencies]
sqlx = { version = "0.8", features = [
    "runtime-tokio",  # Runtime assíncrono
    "tls-rustls",     # TLS via rustls
    "postgres",       # Suporte a PostgreSQL
    "chrono",         # Tipos de data/hora
    "uuid",           # Tipo UUID
    "json",           # Tipo JSON/JSONB
] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }

Para usar queries verificadas em compile-time, você também precisa da CLI:

cargo install sqlx-cli

Features disponíveis por banco de dados:

# PostgreSQL
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres"] }

# MySQL/MariaDB
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "mysql"] }

# SQLite
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "sqlite"] }

Uso Básico

Conectando ao banco de dados

use sqlx::postgres::PgPoolOptions;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    // Criar pool de conexões
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .acquire_timeout(Duration::from_secs(3))
        .connect("postgres://usuario:senha@localhost:5432/meu_banco")
        .await?;

    // Ou usar variável de ambiente DATABASE_URL
    // let pool = PgPoolOptions::new()
    //     .connect(&std::env::var("DATABASE_URL").unwrap())
    //     .await?;

    println!("Conectado ao PostgreSQL!");

    // Teste de conexão
    let resultado: (i64,) = sqlx::query_as("SELECT $1")
        .bind(150_i64)
        .fetch_one(&pool)
        .await?;

    println!("Resultado: {}", resultado.0);

    Ok(())
}

Query simples com query!()

A macro query!() verifica a query SQL em tempo de compilação:

use sqlx::PgPool;

async fn exemplo_query(pool: &PgPool) -> Result<(), sqlx::Error> {
    // query!() verifica a query em compile-time
    // Requer DATABASE_URL configurado no ambiente ou em .env
    let registro = sqlx::query!(
        "SELECT id, nome, email FROM usuarios WHERE id = $1",
        1_i32
    )
    .fetch_one(pool)
    .await?;

    // Os campos são tipados automaticamente!
    println!("ID: {}", registro.id);           // i32
    println!("Nome: {}", registro.nome);       // String
    println!("Email: {}", registro.email);     // String

    Ok(())
}

Mapeamento para structs com FromRow

use sqlx::{FromRow, PgPool};

#[derive(Debug, FromRow)]
struct Usuario {
    id: i32,
    nome: String,
    email: String,
    ativo: bool,
    criado_em: chrono::NaiveDateTime,
}

async fn listar_usuarios(pool: &PgPool) -> Result<Vec<Usuario>, sqlx::Error> {
    // query_as mapeia automaticamente para a struct
    let usuarios = sqlx::query_as::<_, Usuario>(
        "SELECT id, nome, email, ativo, criado_em FROM usuarios ORDER BY nome"
    )
    .fetch_all(pool)
    .await?;

    Ok(usuarios)
}

// Com query_as!() para verificação em compile-time
async fn obter_usuario(pool: &PgPool, id: i32) -> Result<Option<Usuario>, sqlx::Error> {
    let usuario = sqlx::query_as!(
        Usuario,
        "SELECT id, nome, email, ativo, criado_em FROM usuarios WHERE id = $1",
        id
    )
    .fetch_optional(pool)
    .await?;

    Ok(usuario)
}

Inserção, atualização e exclusão

use sqlx::PgPool;

#[derive(Debug)]
struct NovoUsuario {
    nome: String,
    email: String,
}

async fn criar_usuario(pool: &PgPool, novo: &NovoUsuario) -> Result<i32, sqlx::Error> {
    let registro = sqlx::query!(
        r#"
        INSERT INTO usuarios (nome, email, ativo, criado_em)
        VALUES ($1, $2, true, NOW())
        RETURNING id
        "#,
        novo.nome,
        novo.email,
    )
    .fetch_one(pool)
    .await?;

    Ok(registro.id)
}

async fn atualizar_usuario(
    pool: &PgPool,
    id: i32,
    nome: &str,
    email: &str,
) -> Result<bool, sqlx::Error> {
    let resultado = sqlx::query!(
        "UPDATE usuarios SET nome = $1, email = $2 WHERE id = $3",
        nome,
        email,
        id,
    )
    .execute(pool)
    .await?;

    Ok(resultado.rows_affected() > 0)
}

async fn deletar_usuario(pool: &PgPool, id: i32) -> Result<bool, sqlx::Error> {
    let resultado = sqlx::query!(
        "DELETE FROM usuarios WHERE id = $1",
        id,
    )
    .execute(pool)
    .await?;

    Ok(resultado.rows_affected() > 0)
}

Transações

use sqlx::PgPool;

async fn transferir_saldo(
    pool: &PgPool,
    de_conta: i32,
    para_conta: i32,
    valor: f64,
) -> Result<(), sqlx::Error> {
    // Iniciar transação
    let mut tx = pool.begin().await?;

    // Debitar da conta de origem
    let resultado = sqlx::query!(
        "UPDATE contas SET saldo = saldo - $1 WHERE id = $2 AND saldo >= $1",
        valor,
        de_conta,
    )
    .execute(&mut *tx)
    .await?;

    if resultado.rows_affected() == 0 {
        // Rollback automático quando tx é dropado sem commit
        return Err(sqlx::Error::RowNotFound);
    }

    // Creditar na conta de destino
    sqlx::query!(
        "UPDATE contas SET saldo = saldo + $1 WHERE id = $2",
        valor,
        para_conta,
    )
    .execute(&mut *tx)
    .await?;

    // Registrar a transferência
    sqlx::query!(
        r#"
        INSERT INTO transferencias (de_conta, para_conta, valor, data)
        VALUES ($1, $2, $3, NOW())
        "#,
        de_conta,
        para_conta,
        valor,
    )
    .execute(&mut *tx)
    .await?;

    // Commit da transação
    tx.commit().await?;

    println!(
        "Transferência de R${:.2} da conta {} para {} concluída",
        valor, de_conta, para_conta
    );

    Ok(())
}

Recursos Avançados

Migrations

O SQLx possui um sistema de migrations integrado:

# Criar pasta de migrations
sqlx migrate add criar_tabela_usuarios

# Isso cria: migrations/20260227120000_criar_tabela_usuarios.sql

Escreva a migration SQL:

-- migrations/20260227120000_criar_tabela_usuarios.sql

CREATE TABLE usuarios (
    id SERIAL PRIMARY KEY,
    nome VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    senha_hash VARCHAR(255) NOT NULL,
    ativo BOOLEAN NOT NULL DEFAULT true,
    criado_em TIMESTAMP NOT NULL DEFAULT NOW(),
    atualizado_em TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_usuarios_email ON usuarios(email);
CREATE INDEX idx_usuarios_ativo ON usuarios(ativo);

Execute as migrations:

# Executar migrations pendentes
sqlx migrate run

# Reverter última migration
sqlx migrate revert

# Verificar status
sqlx migrate info

Executar migrations no código:

use sqlx::PgPool;
use sqlx::migrate::Migrator;

static MIGRATOR: Migrator = sqlx::migrate!(); // Carrega de ./migrations

async fn executar_migrations(pool: &PgPool) -> Result<(), sqlx::Error> {
    MIGRATOR.run(pool).await?;
    println!("Migrations executadas com sucesso!");
    Ok(())
}

Queries dinâmicas com QueryBuilder

use sqlx::{PgPool, QueryBuilder, Postgres};

#[derive(Debug)]
struct FiltroUsuarios {
    nome: Option<String>,
    email: Option<String>,
    ativo: Option<bool>,
    limite: i64,
    offset: i64,
}

async fn buscar_usuarios(
    pool: &PgPool,
    filtro: &FiltroUsuarios,
) -> Result<Vec<(i32, String, String, bool)>, sqlx::Error> {
    let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
        "SELECT id, nome, email, ativo FROM usuarios WHERE 1=1"
    );

    if let Some(ref nome) = filtro.nome {
        query_builder.push(" AND nome ILIKE ");
        query_builder.push_bind(format!("%{}%", nome));
    }

    if let Some(ref email) = filtro.email {
        query_builder.push(" AND email ILIKE ");
        query_builder.push_bind(format!("%{}%", email));
    }

    if let Some(ativo) = filtro.ativo {
        query_builder.push(" AND ativo = ");
        query_builder.push_bind(ativo);
    }

    query_builder.push(" ORDER BY nome LIMIT ");
    query_builder.push_bind(filtro.limite);
    query_builder.push(" OFFSET ");
    query_builder.push_bind(filtro.offset);

    let query = query_builder.build_query_as::<(i32, String, String, bool)>();
    query.fetch_all(pool).await
}

Inserção em batch

use sqlx::{PgPool, QueryBuilder, Postgres};

struct NovoUsuario {
    nome: String,
    email: String,
}

async fn inserir_em_batch(
    pool: &PgPool,
    usuarios: &[NovoUsuario],
) -> Result<(), sqlx::Error> {
    if usuarios.is_empty() {
        return Ok(());
    }

    let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
        "INSERT INTO usuarios (nome, email, ativo, criado_em) "
    );

    query_builder.push_values(usuarios, |mut b, usuario| {
        b.push_bind(&usuario.nome)
            .push_bind(&usuario.email)
            .push("true")
            .push("NOW()");
    });

    let query = query_builder.build();
    query.execute(pool).await?;

    println!("{} usuários inseridos", usuarios.len());
    Ok(())
}

Streaming de resultados

Para grandes conjuntos de dados, use streams para não carregar tudo na memória:

use sqlx::PgPool;
use futures::TryStreamExt;

async fn processar_todos_usuarios(pool: &PgPool) -> Result<(), sqlx::Error> {
    let mut stream = sqlx::query!(
        "SELECT id, nome, email FROM usuarios ORDER BY id"
    )
    .fetch(pool);

    let mut contagem = 0;

    while let Some(registro) = stream.try_next().await? {
        contagem += 1;
        // Processar cada registro sem carregar todos na memória
        println!("[{}] {} - {}", registro.id, registro.nome, registro.email);
    }

    println!("Total processado: {}", contagem);
    Ok(())
}

Tipos customizados

use sqlx::{Type, FromRow, Decode, Encode, Postgres};

// Enum mapeado para tipo PostgreSQL
#[derive(Debug, Clone, Type)]
#[sqlx(type_name = "status_tarefa", rename_all = "snake_case")]
enum StatusTarefa {
    Pendente,
    EmProgresso,
    Concluida,
    Cancelada,
}

// Struct com tipos customizados
#[derive(Debug, FromRow)]
struct Tarefa {
    id: i32,
    titulo: String,
    status: StatusTarefa,
    prioridade: i16,
    criado_em: chrono::NaiveDateTime,
    metadata: Option<serde_json::Value>, // JSONB
    tags: Vec<String>,                   // TEXT[]
    responsavel_id: Option<uuid::Uuid>,  // UUID
}

async fn criar_tarefa_com_tipos(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
    let tags = vec!["urgente".to_string(), "backend".to_string()];
    let metadata = serde_json::json!({"sprint": 5, "pontos": 3});

    sqlx::query!(
        r#"
        INSERT INTO tarefas (titulo, status, prioridade, tags, metadata)
        VALUES ($1, $2::status_tarefa, $3, $4, $5)
        "#,
        "Implementar cache",
        StatusTarefa::Pendente as StatusTarefa,
        1_i16,
        &tags,
        metadata,
    )
    .execute(pool)
    .await?;

    Ok(())
}

Preparando para compile-time checks offline

# Gerar metadados para verificação offline
# (permite compilar sem conexão ao banco)
cargo sqlx prepare

# Isso cria o diretório .sqlx/ com os metadados das queries
# Comite este diretório no git para CI/CD

# Verificar se os metadados estão atualizados
cargo sqlx prepare --check

Boas Práticas

1. Sempre use connection pooling

use sqlx::postgres::PgPoolOptions;
use std::time::Duration;

async fn criar_pool() -> Result<sqlx::PgPool, sqlx::Error> {
    PgPoolOptions::new()
        // Número máximo de conexões
        .max_connections(20)
        // Tempo mínimo de vida de uma conexão
        .min_connections(5)
        // Timeout para adquirir conexão do pool
        .acquire_timeout(Duration::from_secs(5))
        // Tempo máximo de vida de uma conexão
        .max_lifetime(Duration::from_secs(30 * 60))
        // Tempo máximo ociosa antes de ser fechada
        .idle_timeout(Duration::from_secs(10 * 60))
        // Testar conexão antes de entregar
        .test_before_acquire(true)
        .connect(&std::env::var("DATABASE_URL").expect("DATABASE_URL não definida"))
        .await
}

2. Use .env para configuração local

Crie um arquivo .env na raiz do projeto:

DATABASE_URL=postgres://usuario:senha@localhost:5432/meu_banco
// No início do main
fn main() {
    dotenvy::dotenv().ok();
    // ...
}

3. Separe a camada de repositório

use sqlx::PgPool;

// Trait de repositório para abstração
#[async_trait::async_trait]
trait UsuarioRepo {
    async fn criar(&self, nome: &str, email: &str) -> Result<i32, sqlx::Error>;
    async fn obter(&self, id: i32) -> Result<Option<Usuario>, sqlx::Error>;
    async fn listar(&self, limite: i64) -> Result<Vec<Usuario>, sqlx::Error>;
}

// Implementação concreta com PostgreSQL
struct PgUsuarioRepo {
    pool: PgPool,
}

#[async_trait::async_trait]
impl UsuarioRepo for PgUsuarioRepo {
    async fn criar(&self, nome: &str, email: &str) -> Result<i32, sqlx::Error> {
        let registro = sqlx::query!(
            "INSERT INTO usuarios (nome, email) VALUES ($1, $2) RETURNING id",
            nome,
            email,
        )
        .fetch_one(&self.pool)
        .await?;

        Ok(registro.id)
    }

    async fn obter(&self, id: i32) -> Result<Option<Usuario>, sqlx::Error> {
        sqlx::query_as!(
            Usuario,
            "SELECT id, nome, email, ativo, criado_em FROM usuarios WHERE id = $1",
            id,
        )
        .fetch_optional(&self.pool)
        .await
    }

    async fn listar(&self, limite: i64) -> Result<Vec<Usuario>, sqlx::Error> {
        sqlx::query_as!(
            Usuario,
            "SELECT id, nome, email, ativo, criado_em FROM usuarios ORDER BY nome LIMIT $1",
            limite,
        )
        .fetch_all(&self.pool)
        .await
    }
}

4. Trate erros de banco adequadamente

use sqlx::Error as SqlxError;

#[derive(Debug)]
enum AppErro {
    NaoEncontrado(String),
    Duplicado(String),
    BancoDeDados(SqlxError),
    Validacao(String),
}

impl From<SqlxError> for AppErro {
    fn from(err: SqlxError) -> Self {
        match &err {
            SqlxError::RowNotFound => {
                AppErro::NaoEncontrado("Registro não encontrado".to_string())
            }
            SqlxError::Database(db_err) => {
                // Código de erro PostgreSQL para unique_violation
                if db_err.code().map(|c| c == "23505").unwrap_or(false) {
                    AppErro::Duplicado(
                        db_err.message().to_string()
                    )
                } else {
                    AppErro::BancoDeDados(err)
                }
            }
            _ => AppErro::BancoDeDados(err),
        }
    }
}

5. Use sqlx prepare para CI/CD

# .github/workflows/ci.yml
- name: Verificar queries SQLx
  run: cargo sqlx prepare --check

Exemplos Práticos

CRUD completo com PostgreSQL e Axum

use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::{get, post, put, delete},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};

// === Modelos ===

#[derive(Debug, Serialize, FromRow)]
struct Produto {
    id: i32,
    nome: String,
    descricao: Option<String>,
    preco: f64,
    estoque: i32,
    ativo: bool,
}

#[derive(Debug, Deserialize)]
struct CriarProduto {
    nome: String,
    descricao: Option<String>,
    preco: f64,
    estoque: i32,
}

#[derive(Debug, Deserialize)]
struct AtualizarProduto {
    nome: Option<String>,
    descricao: Option<String>,
    preco: Option<f64>,
    estoque: Option<i32>,
    ativo: Option<bool>,
}

// === Estado da aplicação ===

#[derive(Clone)]
struct AppState {
    pool: PgPool,
}

// === Handlers ===

async fn listar_produtos(
    State(state): State<AppState>,
) -> Result<Json<Vec<Produto>>, StatusCode> {
    let produtos = sqlx::query_as!(
        Produto,
        "SELECT id, nome, descricao, preco, estoque, ativo FROM produtos WHERE ativo = true ORDER BY nome"
    )
    .fetch_all(&state.pool)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok(Json(produtos))
}

async fn obter_produto(
    State(state): State<AppState>,
    Path(id): Path<i32>,
) -> Result<Json<Produto>, StatusCode> {
    let produto = sqlx::query_as!(
        Produto,
        "SELECT id, nome, descricao, preco, estoque, ativo FROM produtos WHERE id = $1",
        id,
    )
    .fetch_optional(&state.pool)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
    .ok_or(StatusCode::NOT_FOUND)?;

    Ok(Json(produto))
}

async fn criar_produto(
    State(state): State<AppState>,
    Json(novo): Json<CriarProduto>,
) -> Result<(StatusCode, Json<Produto>), StatusCode> {
    let produto = sqlx::query_as!(
        Produto,
        r#"
        INSERT INTO produtos (nome, descricao, preco, estoque, ativo)
        VALUES ($1, $2, $3, $4, true)
        RETURNING id, nome, descricao, preco, estoque, ativo
        "#,
        novo.nome,
        novo.descricao,
        novo.preco,
        novo.estoque,
    )
    .fetch_one(&state.pool)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok((StatusCode::CREATED, Json(produto)))
}

async fn atualizar_produto(
    State(state): State<AppState>,
    Path(id): Path<i32>,
    Json(dados): Json<AtualizarProduto>,
) -> Result<Json<Produto>, StatusCode> {
    let produto = sqlx::query_as!(
        Produto,
        r#"
        UPDATE produtos SET
            nome = COALESCE($2, nome),
            descricao = COALESCE($3, descricao),
            preco = COALESCE($4, preco),
            estoque = COALESCE($5, estoque),
            ativo = COALESCE($6, ativo)
        WHERE id = $1
        RETURNING id, nome, descricao, preco, estoque, ativo
        "#,
        id,
        dados.nome,
        dados.descricao,
        dados.preco,
        dados.estoque,
        dados.ativo,
    )
    .fetch_optional(&state.pool)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
    .ok_or(StatusCode::NOT_FOUND)?;

    Ok(Json(produto))
}

async fn deletar_produto(
    State(state): State<AppState>,
    Path(id): Path<i32>,
) -> Result<StatusCode, StatusCode> {
    let resultado = sqlx::query!(
        "DELETE FROM produtos WHERE id = $1",
        id,
    )
    .execute(&state.pool)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    if resultado.rows_affected() == 0 {
        return Err(StatusCode::NOT_FOUND);
    }

    Ok(StatusCode::NO_CONTENT)
}

// === Ponto de entrada ===

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenvy::dotenv().ok();

    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL deve estar definida");

    let pool = sqlx::postgres::PgPoolOptions::new()
        .max_connections(10)
        .connect(&database_url)
        .await?;

    // Executar migrations
    sqlx::migrate!().run(&pool).await?;

    let state = AppState { pool };

    let app = Router::new()
        .route("/produtos", get(listar_produtos).post(criar_produto))
        .route(
            "/produtos/{id}",
            get(obter_produto)
                .put(atualizar_produto)
                .delete(deletar_produto),
        )
        .with_state(state);

    println!("Servidor rodando em http://0.0.0.0:3000");
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}

Comparação com Alternativas

CaracterísticaSQLxDieselSeaORMRusqlite
AbordagemSQL puro verificadoORM com DSLORM com ActiveModelBindings SQLite
Compile-time checkSim (query!())Sim (schema DSL)NãoNão
AsyncSim (nativo)Não (sync)SimNão (sync)
Bancos suportadosPG, MySQL, SQLitePG, MySQL, SQLitePG, MySQL, SQLiteApenas SQLite
SQL puroSimNão (DSL)OpcionalSim
MigrationsIntegradodiesel_cliIntegrado (sea-orm-cli)Manual
Connection poolEmbutidoVia r2d2EmbutidoNão aplicável
Curva de aprendizadoBaixa (sabe SQL)Média (aprender DSL)MédiaBaixa
  • SQLx vs Diesel: SQLx usa SQL puro e é assíncrono. Diesel usa uma DSL tipada e é síncrono. Se prefere SQL puro e precisa de async, escolha SQLx. Se quer mais abstração e type-safety no query building, escolha Diesel.
  • SQLx vs SeaORM: SeaORM é construído sobre SQLx e adiciona uma camada ORM. Use SeaORM se prefere o padrão ActiveModel/ActiveRecord.
  • SQLx vs Rusqlite: Rusqlite é focado exclusivamente em SQLite e é síncrono. Se só precisa de SQLite local sem async, Rusqlite é mais simples.

Conclusão

O SQLx oferece o melhor dos dois mundos: a flexibilidade do SQL puro com a segurança de tipos de um ORM. A verificação em compile-time é uma funcionalidade revolucionária que elimina uma classe inteira de bugs em tempo de execução.

Para a maioria dos projetos web em Rust, o SQLx é a escolha mais equilibrada: poderoso o suficiente para qualquer query complexa, seguro o suficiente para produção, e ergonômico o suficiente para não atrapalhar o desenvolvimento.

SQLx em produção: checklist rápido

Antes de colocar uma API Rust com SQLx em produção, valide os pontos que costumam separar tutorial de sistema operável:

  • Configure DATABASE_URL por ambiente e nunca grave credenciais no repositório.
  • Use PgPoolOptions com limite de conexões compatível com o banco, não com a quantidade de requests esperada.
  • Rode sqlx migrate run no pipeline ou em uma etapa controlada de deploy, com rollback planejado para mudanças destrutivas.
  • Ative modo offline (cargo sqlx prepare) quando o build não puder acessar o banco de desenvolvimento.
  • Traduza erros de banco para respostas de API claras: conflito, não encontrado, validação ou erro interno.
  • Instrumente queries lentas com Tracing e exponha latência por rota quando usar Axum.
  • Teste transações, constraints e concorrência; muitos bugs de banco só aparecem quando duas requisições disputam o mesmo registro.
  • Documente quais tabelas pertencem a cada serviço se a aplicação crescer para microsserviços Rust.

Se você está montando portfólio, um bom projeto é uma API de produtos ou pagamentos com Axum, SQLx, PostgreSQL, migrations versionadas, testes de integração, Docker Compose e logs estruturados. Esse tipo de projeto demonstra o que recrutadores procuram em backend Rust: domínio de banco, HTTP, erros, deploy e observabilidade.

Próximos passos