Connect Rust to Xata

Learn how to connect your Rust application to Xata's PostgreSQL platform. Get started with Rust and PostgreSQL for high-performance, memory-safe applications.

Prerequisites

  • Xata account and project setup
  • Rust 1.70+ installed
  • Basic knowledge of Rust

Setup Xata Database

First, set up your Xata database with the common e-commerce dataset:

  1. Create a project and branch in the Xata console
  2. Navigate to the Queries tab in your branch
  3. Run the following SQL commands to create the initial schema:
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  price NUMERIC(7,2) NOT NULL,
  rating INTEGER
);

CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE order_items (
  order_id INTEGER REFERENCES orders(id),
  product_id INTEGER REFERENCES products(id),
  qty INTEGER NOT NULL,
  PRIMARY KEY (order_id, product_id)
);

Create Rust Project

Create a new Rust project:

cargo new my-xata-app
cd my-xata-app

Initialize Xata Project

Initialize your Xata project configuration:

xata init

This will create a .xata directory with your project configuration.

Store Your Credentials

Create a .env file in your project root to store your Xata connection string:

# .env
DATABASE_URL="postgresql://username:password@host:port/database"

Get your connection string from the Xata console or CLI:

xata branch url

Important: Never commit your .env file to version control. Add it to your .gitignore file.

Configure Dependencies

Update your Cargo.toml:

[package]
name = "my-xata-app"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "decimal"] }
tokio = { version = "1.0", features = ["full"] }
chrono = { version = "0.4", features = ["serde"] }
rust_decimal = { version = "1.32", features = ["serde"] }
dotenv = "0.15"

Create Models

Create your data structures:

// src/models.rs
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;

#[derive(Debug, Serialize, Deserialize)]
pub struct Product {
    pub id: i32,
    pub name: String,
    pub price: Decimal,
    pub rating: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CreateProduct {
    pub name: String,
    pub price: Decimal,
    pub rating: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Order {
    pub id: i32,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct OrderItem {
    pub order_id: i32,
    pub product_id: i32,
    pub qty: i32,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct OrderWithProducts {
    pub order_id: i32,
    pub created_at: DateTime<Utc>,
    pub product_name: String,
    pub qty: i32,
    pub total: Decimal,
}

Create Database Module

Create database connection and queries:

// src/db.rs
use sqlx::PgPool;
use crate::models::{Product, CreateProduct, OrderWithProducts};

pub async fn create_pool() -> Result<PgPool, sqlx::Error> {
    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    
    PgPool::connect(&database_url).await
}

pub async fn get_products(pool: &PgPool) -> Result<Vec<Product>, sqlx::Error> {
    sqlx::query_as!(
        Product,
        "SELECT id, name, price, rating FROM products"
    )
    .fetch_all(pool)
    .await
}

pub async fn get_product(pool: &PgPool, id: i32) -> Result<Option<Product>, sqlx::Error> {
    sqlx::query_as!(
        Product,
        "SELECT id, name, price, rating FROM products WHERE id = $1",
        id
    )
    .fetch_optional(pool)
    .await
}

pub async fn create_product(pool: &PgPool, product: CreateProduct) -> Result<Product, sqlx::Error> {
    sqlx::query_as!(
        Product,
        "INSERT INTO products (name, price, rating) VALUES ($1, $2, $3) RETURNING id, name, price, rating",
        product.name,
        product.price,
        product.rating
    )
    .fetch_one(pool)
    .await
}

pub async fn get_orders_with_products(pool: &PgPool) -> Result<Vec<OrderWithProducts>, sqlx::Error> {
    sqlx::query_as!(
        OrderWithProducts,
        r#"
        SELECT 
            o.id as order_id,
            o.created_at,
            p.name as product_name,
            oi.qty,
            p.price * oi.qty as total
        FROM orders o
        JOIN order_items oi ON o.id = oi.order_id
        JOIN products p ON oi.product_id = p.id
        "#
    )
    .fetch_all(pool)
    .await
}

Create Handlers

Create HTTP handlers:

// src/handlers.rs
use actix_web::{web, HttpResponse, Result};
use sqlx::PgPool;
use crate::models::{CreateProduct, Product, OrderWithProducts};
use crate::db;

pub async fn get_products(pool: web::Data<PgPool>) -> Result<HttpResponse> {
    match db::get_products(&pool).await {
        Ok(products) => Ok(HttpResponse::Ok().json(products)),
        Err(e) => {
            eprintln!("Database error: {}", e);
            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
                "error": "Failed to fetch products"
            })))
        }
    }
}

pub async fn get_product(
    pool: web::Data<PgPool>,
    path: web::Path<i32>
) -> Result<HttpResponse> {
    let id = path.into_inner();
    
    match db::get_product(&pool, id).await {
        Ok(Some(product)) => Ok(HttpResponse::Ok().json(product)),
        Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({
            "error": "Product not found"
        }))),
        Err(e) => {
            eprintln!("Database error: {}", e);
            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
                "error": "Failed to fetch product"
            })))
        }
    }
}

pub async fn create_product(
    pool: web::Data<PgPool>,
    product: web::Json<CreateProduct>
) -> Result<HttpResponse> {
    match db::create_product(&pool, product.into_inner()).await {
        Ok(product) => Ok(HttpResponse::Created().json(product)),
        Err(e) => {
            eprintln!("Database error: {}", e);
            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
                "error": "Failed to create product"
            })))
        }
    }
}

pub async fn get_orders(pool: web::Data<PgPool>) -> Result<HttpResponse> {
    match db::get_orders_with_products(&pool).await {
        Ok(orders) => Ok(HttpResponse::Ok().json(orders)),
        Err(e) => {
            eprintln!("Database error: {}", e);
            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
                "error": "Failed to fetch orders"
            })))
        }
    }
}

Create Main Application

Create your main application file:

// src/main.rs
use actix_web::{web, App, HttpServer};
use dotenv::dotenv;

mod models;
mod db;
mod handlers;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();
    
    let pool = db::create_pool()
        .await
        .expect("Failed to create database pool");

    println!("Server starting on http://localhost:3000");

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(pool.clone()))
            .service(
                web::scope("/api")
                    .route("/products", web::get().to(handlers::get_products))
                    .route("/products/{id}", web::get().to(handlers::get_product))
                    .route("/products", web::post().to(handlers::create_product))
                    .route("/orders", web::get().to(handlers::get_orders))
            )
    })
    .bind("127.0.0.1:3000")?
    .run()
    .await
}

Run the Application

Start your development server:

cargo run

Your API will be available at:

  • http://localhost:3000/api/products
  • http://localhost:3000/api/orders

Test Your API

You can test the endpoints using curl:

# Get all products
curl http://localhost:3000/api/products

# Create a new product
curl -X POST http://localhost:3000/api/products \
  -H "Content-Type: application/json" \
  -d '{"name":"Wireless Headphones","price":"99.99","rating":5}'

# Get orders with products
curl http://localhost:3000/api/orders

Next Steps