Rust Web Development with Axum and SQLx

Build high-performance web APIs using Rust, Axum framework, and SQLx for database operations

# Rust Web Development with Axum and SQLx

## 1. Project Setup and Dependencies

### Cargo.toml Configuration
```toml
[package]
name = "rust-web-api"
version = "0.1.0"
edition = "2021"

[dependencies]
# Web framework
axum = { version = "0.7", features = ["macros"] }
tokio = { version = "1.0", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }

# Database
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "migrate"] }

# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Error handling
anyhow = "1.0"
thiserror = "1.0"

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Validation
validator = { version = "0.18", features = ["derive"] }

# Authentication
jsonwebtoken = "9.2"
argon2 = "0.5"

# Environment
dotenvy = "0.15"

# UUID and time
uuid = { version = "1.0", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }

[dev-dependencies]
httptest = "0.15"
```

### Project Structure
```
src/
├── main.rs              # Application entry point
├── lib.rs               # Library root
├── config/              # Configuration management
│   └── mod.rs
├── handlers/            # HTTP request handlers
│   ├── mod.rs
│   ├── auth.rs
│   └── users.rs
├── models/              # Data models
│   ├── mod.rs
│   └── user.rs
├── middleware/          # Custom middleware
│   ├── mod.rs
│   └── auth.rs
├── database/            # Database setup and migrations
│   ├── mod.rs
│   └── connection.rs
├── services/            # Business logic
│   ├── mod.rs
│   └── user_service.rs
├── utils/               # Utility functions
│   ├── mod.rs
│   └── password.rs
└── errors/              # Error types
    └── mod.rs
migrations/              # Database migrations
tests/                   # Integration tests
```

## 2. Database Setup with SQLx

### Database Configuration
```rust
// src/config/mod.rs
use serde::Deserialize;

#[derive(Debug, Deserialize, Clone)]
pub struct Config {
    pub database_url: String,
    pub jwt_secret: String,
    pub server_port: u16,
    pub log_level: String,
}

impl Config {
    pub fn from_env() -> Result<Self, anyhow::Error> {
        dotenvy::dotenv().ok();

        Ok(Config {
            database_url: std::env::var("DATABASE_URL")
                .map_err(|_| anyhow::anyhow!("DATABASE_URL must be set"))?,
            jwt_secret: std::env::var("JWT_SECRET")
                .map_err(|_| anyhow::anyhow!("JWT_SECRET must be set"))?,
            server_port: std::env::var("PORT")
                .unwrap_or_else(|_| "3000".to_string())
                .parse()?,
            log_level: std::env::var("LOG_LEVEL")
                .unwrap_or_else(|_| "info".to_string()),
        })
    }
}
```

### Database Connection
```rust
// src/database/connection.rs
use sqlx::{PgPool, Pool, Postgres};
use tracing::info;

pub type DatabaseConnection = Pool<Postgres>;

pub async fn create_connection_pool(database_url: &str) -> Result<DatabaseConnection, sqlx::Error> {
    info!("Connecting to database...");

    let pool = PgPool::connect(database_url).await?;

    info!("Database connection established");
    Ok(pool)
}

pub async fn run_migrations(pool: &DatabaseConnection) -> Result<(), sqlx::Error> {
    info!("Running database migrations...");
    sqlx::migrate!("./migrations").run(pool).await?;
    info!("Migrations completed successfully");
    Ok(())
}
```

### User Model
```rust
// src/models/user.rs
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
use validator::Validate;

#[derive(Debug, Serialize, FromRow)]
pub struct User {
    pub id: Uuid,
    pub email: String,
    pub username: String,
    pub password_hash: String,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize, Validate)]
pub struct CreateUserRequest {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 3, max = 50))]
    pub username: String,
    #[validate(length(min = 8))]
    pub password: String,
}

#[derive(Debug, Deserialize, Validate)]
pub struct LoginRequest {
    #[validate(email)]
    pub email: String,
    pub password: String,
}

#[derive(Debug, Serialize)]
pub struct UserResponse {
    pub id: Uuid,
    pub email: String,
    pub username: String,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
}

impl From<User> for UserResponse {
    fn from(user: User) -> Self {
        Self {
            id: user.id,
            email: user.email,
            username: user.username,
            is_active: user.is_active,
            created_at: user.created_at,
        }
    }
}

#[derive(Debug, Serialize)]
pub struct AuthResponse {
    pub token: String,
    pub user: UserResponse,
}
```

## 3. Error Handling

### Custom Error Types
```rust
// src/errors/mod.rs
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("Validation error: {0}")]
    Validation(String),

    #[error("Authentication error: {0}")]
    Authentication(String),

    #[error("Authorization error: {0}")]
    Authorization(String),

    #[error("Not found: {0}")]
    NotFound(String),

    #[error("Internal server error: {0}")]
    Internal(#[from] anyhow::Error),

    #[error("Bad request: {0}")]
    BadRequest(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            AppError::Database(ref err) => {
                tracing::error!("Database error: {:?}", err);
                (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
            }
            AppError::Validation(ref msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
            AppError::Authentication(ref msg) => (StatusCode::UNAUTHORIZED, msg.as_str()),
            AppError::Authorization(ref msg) => (StatusCode::FORBIDDEN, msg.as_str()),
            AppError::NotFound(ref msg) => (StatusCode::NOT_FOUND, msg.as_str()),
            AppError::Internal(ref err) => {
                tracing::error!("Internal error: {:?}", err);
                (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
            }
            AppError::BadRequest(ref msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
        };

        let body = Json(json!({
            "error": error_message,
            "status": status.as_u16()
        }));

        (status, body).into_response()
    }
}

pub type Result<T> = std::result::Result<T, AppError>;
```

## 4. Authentication and JWT

### Password Utilities
```rust
// src/utils/password.rs
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Argon2,
};
use crate::errors::{AppError, Result};

pub fn hash_password(password: &str) -> Result<String> {
    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();

    let password_hash = argon2
        .hash_password(password.as_bytes(), &salt)
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to hash password: {}", e)))?
        .to_string();

    Ok(password_hash)
}

pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
    let parsed_hash = PasswordHash::new(hash)
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Invalid password hash: {}", e)))?;

    let argon2 = Argon2::default();

    Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
}
```

### JWT Middleware
```rust
// src/middleware/auth.rs
use axum::{
    extract::{Request, State},
    http::header::AUTHORIZATION,
    middleware::Next,
    response::Response,
};
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{errors::AppError, AppState};

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String, // User ID
    pub exp: usize,  // Expiration time
    pub iat: usize,  // Issued at
}

pub async fn auth_middleware(
    State(state): State<AppState>,
    mut request: Request,
    next: Next,
) -> Result<Response, AppError> {
    let auth_header = request
        .headers()
        .get(AUTHORIZATION)
        .and_then(|header| header.to_str().ok())
        .ok_or_else(|| AppError::Authentication("Missing authorization header".to_string()))?;

    let token = auth_header
        .strip_prefix("Bearer ")
        .ok_or_else(|| AppError::Authentication("Invalid authorization header format".to_string()))?;

    let claims = decode::<Claims>(
        token,
        &DecodingKey::from_secret(state.config.jwt_secret.as_ref()),
        &Validation::default(),
    )
    .map_err(|_| AppError::Authentication("Invalid token".to_string()))?;

    let user_id = Uuid::parse_str(&claims.sub)
        .map_err(|_| AppError::Authentication("Invalid user ID in token".to_string()))?;

    // Add user ID to request extensions
    request.extensions_mut().insert(user_id);

    Ok(next.run(request).await)
}
```

## 5. Services Layer

### User Service
```rust
// src/services/user_service.rs
use uuid::Uuid;
use chrono::Utc;
use sqlx::PgPool;

use crate::{
    models::user::{User, CreateUserRequest, UserResponse},
    utils::password::{hash_password, verify_password},
    errors::{AppError, Result},
};

pub struct UserService {
    db: PgPool,
}

impl UserService {
    pub fn new(db: PgPool) -> Self {
        Self { db }
    }

    pub async fn create_user(&self, request: CreateUserRequest) -> Result<UserResponse> {
        // Check if user already exists
        let existing = sqlx::query_as::<_, User>(
            "SELECT * FROM users WHERE email = $1 OR username = $2"
        )
        .bind(&request.email)
        .bind(&request.username)
        .fetch_optional(&self.db)
        .await?;

        if existing.is_some() {
            return Err(AppError::BadRequest("User already exists".to_string()));
        }

        // Hash password
        let password_hash = hash_password(&request.password)?;

        // Create user
        let user = sqlx::query_as::<_, User>(
            r#"
            INSERT INTO users (id, email, username, password_hash, is_active, created_at, updated_at)
            VALUES ($1, $2, $3, $4, $5, $6, $7)
            RETURNING *
            "#
        )
        .bind(Uuid::new_v4())
        .bind(&request.email)
        .bind(&request.username)
        .bind(&password_hash)
        .bind(true)
        .bind(Utc::now())
        .bind(Utc::now())
        .fetch_one(&self.db)
        .await?;

        Ok(user.into())
    }

    pub async fn authenticate_user(&self, email: &str, password: &str) -> Result<User> {
        let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = $1 AND is_active = true")
            .bind(email)
            .fetch_optional(&self.db)
            .await?
            .ok_or_else(|| AppError::Authentication("Invalid credentials".to_string()))?;

        if !verify_password(password, &user.password_hash)? {
            return Err(AppError::Authentication("Invalid credentials".to_string()));
        }

        Ok(user)
    }

    pub async fn get_user_by_id(&self, id: Uuid) -> Result<UserResponse> {
        let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1 AND is_active = true")
            .bind(id)
            .fetch_optional(&self.db)
            .await?
            .ok_or_else(|| AppError::NotFound("User not found".to_string()))?;

        Ok(user.into())
    }

    pub async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<UserResponse>> {
        let users = sqlx::query_as::<_, User>(
            "SELECT * FROM users WHERE is_active = true ORDER BY created_at DESC LIMIT $1 OFFSET $2"
        )
        .bind(limit)
        .bind(offset)
        .fetch_all(&self.db)
        .await?;

        Ok(users.into_iter().map(|user| user.into()).collect())
    }
}
```

## 6. HTTP Handlers

### Authentication Handlers
```rust
// src/handlers/auth.rs
use axum::{extract::State, http::StatusCode, Json};
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey, Header};
use validator::Validate;

use crate::{
    models::user::{AuthResponse, CreateUserRequest, LoginRequest},
    middleware::auth::Claims,
    services::user_service::UserService,
    errors::{AppError, Result},
    AppState,
};

pub async fn register(
    State(state): State<AppState>,
    Json(request): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<AuthResponse>)> {
    // Validate request
    request.validate()
        .map_err(|e| AppError::Validation(format!("Validation error: {}", e)))?;

    let user_service = UserService::new(state.db.clone());
    let user = user_service.create_user(request).await?;

    // Generate JWT token
    let claims = Claims {
        sub: user.id.to_string(),
        exp: (Utc::now() + Duration::hours(24)).timestamp() as usize,
        iat: Utc::now().timestamp() as usize,
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(state.config.jwt_secret.as_ref()),
    )
    .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to generate token: {}", e)))?;

    let response = AuthResponse { token, user };

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

pub async fn login(
    State(state): State<AppState>,
    Json(request): Json<LoginRequest>,
) -> Result<Json<AuthResponse>> {
    // Validate request
    request.validate()
        .map_err(|e| AppError::Validation(format!("Validation error: {}", e)))?;

    let user_service = UserService::new(state.db.clone());
    let user = user_service.authenticate_user(&request.email, &request.password).await?;

    // Generate JWT token
    let claims = Claims {
        sub: user.id.to_string(),
        exp: (Utc::now() + Duration::hours(24)).timestamp() as usize,
        iat: Utc::now().timestamp() as usize,
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(state.config.jwt_secret.as_ref()),
    )
    .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to generate token: {}", e)))?;

    let response = AuthResponse {
        token,
        user: user.into(),
    };

    Ok(Json(response))
}
```

### User Handlers
```rust
// src/handlers/users.rs
use axum::{
    extract::{Query, Request, State},
    Extension,
    Json,
};
use serde::Deserialize;
use uuid::Uuid;

use crate::{
    models::user::UserResponse,
    services::user_service::UserService,
    errors::Result,
    AppState,
};

#[derive(Deserialize)]
pub struct ListUsersQuery {
    #[serde(default = "default_limit")]
    pub limit: i64,
    #[serde(default)]
    pub offset: i64,
}

fn default_limit() -> i64 {
    20
}

pub async fn get_current_user(
    State(state): State<AppState>,
    Extension(user_id): Extension<Uuid>,
) -> Result<Json<UserResponse>> {
    let user_service = UserService::new(state.db.clone());
    let user = user_service.get_user_by_id(user_id).await?;
    Ok(Json(user))
}

pub async fn list_users(
    State(state): State<AppState>,
    Query(query): Query<ListUsersQuery>,
) -> Result<Json<Vec<UserResponse>>> {
    let user_service = UserService::new(state.db.clone());
    let users = user_service.list_users(query.limit.min(100), query.offset).await?;
    Ok(Json(users))
}
```

## 7. Application Setup

### Main Application
```rust
// src/main.rs
use axum::{
    middleware,
    routing::{get, post},
    Router,
};
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

mod config;
mod database;
mod errors;
mod handlers;
mod middleware;
mod models;
mod services;
mod utils;

use config::Config;
use database::connection::{create_connection_pool, run_migrations};

#[derive(Clone)]
pub struct AppState {
    pub db: sqlx::PgPool,
    pub config: Config,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize tracing
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "rust_web_api=debug,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    // Load configuration
    let config = Config::from_env()?;

    // Create database connection pool
    let db = create_connection_pool(&config.database_url).await?;

    // Run migrations
    run_migrations(&db).await?;

    // Create application state
    let state = AppState { db, config: config.clone() };

    // Create router
    let app = create_router(state);

    // Start server
    let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.server_port)).await?;
    tracing::info!("Server starting on port {}", config.server_port);

    axum::serve(listener, app).await?;

    Ok(())
}

fn create_router(state: AppState) -> Router {
    // Public routes
    let public_routes = Router::new()
        .route("/auth/register", post(handlers::auth::register))
        .route("/auth/login", post(handlers::auth::login));

    // Protected routes
    let protected_routes = Router::new()
        .route("/users/me", get(handlers::users::get_current_user))
        .route("/users", get(handlers::users::list_users))
        .route_layer(middleware::from_fn_with_state(
            state.clone(),
            middleware::auth::auth_middleware,
        ));

    Router::new()
        .nest("/api", public_routes.merge(protected_routes))
        .layer(
            ServiceBuilder::new()
                .layer(TraceLayer::new_for_http())
                .layer(CorsLayer::permissive()),
        )
        .with_state(state)
}
```

## 8. Database Migrations

### Initial Migration
```sql
-- migrations/20240101000000_create_users_table.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) UNIQUE NOT NULL,
    username VARCHAR(50) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_created_at ON users(created_at);

-- Function to automatically update updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

-- Trigger to automatically update updated_at
CREATE TRIGGER update_users_updated_at BEFORE UPDATE
ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
```

## 9. Testing

### Integration Tests
```rust
// tests/integration_tests.rs
use axum::{
    body::Body,
    http::{Request, StatusCode},
    Router,
};
use serde_json::json;
use tower::ServiceExt;

use rust_web_api::{create_router, AppState, Config};

async fn setup_test_app() -> Router {
    let config = Config {
        database_url: "postgresql://test:test@localhost/test_db".to_string(),
        jwt_secret: "test_secret".to_string(),
        server_port: 3000,
        log_level: "debug".to_string(),
    };

    let db = sqlx::PgPool::connect(&config.database_url).await.unwrap();
    let state = AppState { db, config };

    create_router(state)
}

#[tokio::test]
async fn test_user_registration() {
    let app = setup_test_app().await;

    let request_body = json!({
        "email": "test@example.com",
        "username": "testuser",
        "password": "password123"
    });

    let response = app
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/api/auth/register")
                .header("content-type", "application/json")
                .body(Body::from(request_body.to_string()))
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::CREATED);
}

#[tokio::test]
async fn test_user_login() {
    let app = setup_test_app().await;

    // First register a user
    let register_body = json!({
        "email": "test@example.com",
        "username": "testuser",
        "password": "password123"
    });

    app.clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/api/auth/register")
                .header("content-type", "application/json")
                .body(Body::from(register_body.to_string()))
                .unwrap(),
        )
        .await
        .unwrap();

    // Then login
    let login_body = json!({
        "email": "test@example.com",
        "password": "password123"
    });

    let response = app
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/api/auth/login")
                .header("content-type", "application/json")
                .body(Body::from(login_body.to_string()))
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);
}
```

## Checklist for Rust Web Development

- [ ] Set up proper project structure with modules
- [ ] Configure SQLx with database connection pooling
- [ ] Implement comprehensive error handling with custom types
- [ ] Set up JWT authentication with secure password hashing
- [ ] Create service layer for business logic separation
- [ ] Implement input validation using validator crate
- [ ] Add proper logging with tracing
- [ ] Write database migrations for schema management
- [ ] Implement middleware for authentication and CORS
- [ ] Add comprehensive integration tests
- [ ] Configure environment variables properly
- [ ] Set up proper HTTP status codes and responses
- [ ] Implement pagination for list endpoints
- [ ] Add proper documentation with rustdoc
Rust Web Development with Axum and SQLx - Cursor IDE AI Rule