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:
- Create a project and branch in the Xata console
- Navigate to the Queries tab in your branch
- 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
- Explore Xata branching for development workflows
- Learn about schema changes with zero downtime
- Join our Discord community for help and support