Production-ready async patterns using Axum, Tokio, and Solana clients for building backend services, indexers, and APIs.
git clone https://github.com/solanabr/solana-ai-kit.git--- name: backend-async description: Production-ready async patterns using Axum, Tokio, and Solana clients for building backend services, indexers, and APIs. --- # Backend Async Patterns (Rust) Production-ready async patterns using Axum 0.8+, Tokio, and Solana clients for building backend services, indexers, and APIs. ## Modern Stack (2026) - **Axum 0.8+**: Web framework (no more `#[async_trait]` needed!) - **Tokio 1.40+**: Async runtime - **Tower**: Middleware (compression, tracing, timeouts) - **sqlx**: Async database with compile-time checked queries - **solana-client**: Async Solana RPC client - **Redis**: Caching layer for RPC responses ## Axum 0.8 Server Setup ### Basic Server Pattern ```rust use axum::{ Router, routing::{get, post}, extract::{State, Path}, response::IntoResponse, http::StatusCode, Json, }; use tokio::net::TcpListener; use tower_http::{ trace::TraceLayer, compression::CompressionLayer, }; #[derive(Clone)] struct AppState { db: sqlx::PgPool, solana_client: Arc<RpcClient>, redis: redis::Client, } #[tokio::main] async fn main() -> anyhow::Result<()> { // Tracing setup tracing_subscriber::fmt::init(); // Database pool let db = sqlx::postgres::PgPoolOptions::new() .max_connections(50) .connect(&env::var("DATABASE_URL")?) .await?; // Solana client let solana_client = Arc::new(RpcClient::new_with_commitment( env::var("SOLANA_RPC_URL")?, CommitmentConfig::confirmed(), )); // Redis for caching let redis = redis::Client::open(env::var("REDIS_URL")?)?; let state = AppState { db, solana_client, redis }; // Axum 0.8: New path syntax with {} let app = Router::new() .route("/health", get(health_check)) .route("/api/accounts/{pubkey}", get(get_account)) .route("/api/transactions", post(submit_transaction)) .route("/api/program/{program_id}/accounts", get(get_program_accounts)) .layer(TraceLayer::new_for_http()) .layer(CompressionLayer::new()) .with_state(state); let listener = TcpListener::bind("0.0.0.0:3000").await?; tracing::info!("Server listening on {}", listener.local_addr()?); axum::serve(listener, app).await?; Ok(()) } ``` ## Handler Patterns (No #[async_trait] Needed!) ### Modern Handler (Rust 1.75+) ```rust use axum::extract::{State, Path, Json}; use serde::{Deserialize, Serialize}; // ✅ MODERN - No #[async_trait] macro needed! async fn get_user( State(state): State<AppState>, Path(user_id): Path<i64>, ) -> Result<Json<User>, AppError> { let user = sqlx::query_as!( User, "SELECT * FROM users WHERE id = $1", user_id ) .fetch_one(&state.db) .await?; Ok(Json(user)) } async fn create_user( State(state): State<AppState>, Json(payload): Json<CreateUserRequest>, ) -> Result<(StatusCode, Json<User>), AppError> { let user = sqlx::query_as!( User, r#" INSERT INTO users (name, wallet_address) VALUES ($1, $2) RETURNING * "#, payload.name, payload.wallet_address, ) .fetch_one(&state.db) .await?; Ok((StatusCode::CREATED, Json(user))) } ``` ## Error Handling Pattern ### Custom Error Type with IntoResponse ```rust use axum::{ response::{IntoResponse, Response}, http::StatusCode, Json, }; use serde_json::json; #[derive(Debug)] pub enum AppError { Database(sqlx::Error), Solana(solana_client::client_error::ClientError), NotFound(String), InvalidInput(String), Internal(String), } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match self { AppError::Database(e) => { tracing::error!("Database error: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string()) } AppError::Solana(e) => { tracing::error!("Solana RPC error: {:?}", e); (StatusCode::BAD_GATEWAY, "Solana RPC error".to_string()) } AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), AppError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg), AppError::Internal(msg) => { tracing::error!("Internal error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, "Internal error".to_string()) } }; (status, Json(json!({ "error": message }))).into_response() } } // Automatic error conversions impl From<sqlx::Error> for AppError { fn from(e: sqlx::Error) -> Self { AppError::Database(e) } } impl From<solana_client::client_error::ClientError> for AppError { fn from(e: solana_client::client_error::ClientError) -> Self { AppError::Solana(e) } } pub type Result<T> = std::result::Result<T, AppError>; ``` ## Solana Client Integration ### Async Solana RPC Calls ```rust use solana_client::rpc_client::RpcClient; use solana_sdk::{ pubkey::Pubkey, commitment_config::CommitmentConfig, }; async fn get_account_data( State(state): State<AppState>, Path(pubkey): Path<String>, ) -> Result<Json<AccountData>> { // Parse pubkey let pubkey = pubkey.parse::<Pubkey>() .map_err(|_| AppError::InvalidInput("Invalid Solana address".into()))?; // Fetch account (blocks async runtime, use spawn_blocking!) let solana_client = state.solana_client.clone(); let account = tokio::task::spawn_blocking(move || { solana_client.get_account(&pubkey) }) .await .map_err(|e| AppError::Internal(format!("Task join error: {}", e)))? .map_err(AppError::Solana)?; Ok(Json(AccountData { pubkey: pubkey.to_string(), lamports: account.lamports, owner: account.owner.to_string(), })) } ``` ### Anchor Program Client Integration ```rust use anchor_client::{ Client, Cluster, Program, }; use solana_sdk::signature::Keypair; async fn call_program_instruction( state: &AppState, authority: &Keypair, ) -> Result<String> { // Setup client (blocking operation!) let client = tokio::task::spawn_blocking({ let authority = authority.insecure_clone(); move || { Client::new_with_options( Cluster::Mainnet, Rc::new(authority), CommitmentConfig::confirmed(), ) } }).await.map_err(|e| AppError::Internal(e.to_string()))?; let program = client.program(program_id) .map_err(|e| AppError::Solana(e))?; // Call instruction let signature = tokio::task::spawn_blocking(move || { program .request() .accounts(/* ... */) .args(/* ... */) .send() }) .await .map_err(|e| AppError::Internal(e.to_string()))? .map_err(AppError::Solana)?; Ok(signature.to_string()) } ``` ## Avoiding Blocking Operations ### CRITICAL: Don't block the async runtime! ```rust // ❌ BAD - blocks async runtime async fn bad_handler() -> Result<String> { let content = std::fs::read_to_string("file.txt")?; // BLOCKING! Ok(content) } // ✅ GOOD - async file I/O async fn good_async() -> Result<String> { let content = tokio::fs::read_to_string("file.txt").await?; Ok(content) } // ✅ GOOD - spawn_blocking for CPU-intensive work async fn good_blocking() -> Result<u64> { let result = tokio::task::spawn_blocking(|| { expensive_computation() // CPU-intensive }).await?; Ok(result) } ``` ### Latency Guidelines - **<10μs**: Fine in async - **10-100μs**: Consider async alternatives - **>100μs**: MUST use `spawn_blocking` ## Database Patterns ### Compile-Time Checked Queries ```rust use sqlx::{PgPool, FromRow}; #[derive(FromRow, Serialize)] struct Transaction { id: i64, signature: String, amount: i64, created_at: chrono::DateTime<chrono::Utc>, } async fn get_recent_transactions( State(state): State<AppState>, Path(limit): Path<i64>, ) -> Result<Json<Vec<Transaction>>> { // sqlx checks this query at compile time! let txs = sqlx::query_as!( Transaction, r#" SELECT id, signature, amount, created_at FROM transactions ORDER BY created_at DESC LIMIT $1 "#, limit ) .fetch_all(&state.db) .await?; Ok(Json(txs)) } ``` ### Transaction Pattern ```rust use sqlx::{Postgres, Transaction as SqlxTransaction}; async fn complex_operation( db: &PgPool, user_id: i64, amount: i64, ) -> Result<()> { // Start transaction let mut tx: SqlxTransaction<Postgres> = db.begin().await?; // Multiple operations atomically sqlx::query!( "UPDATE accounts SET balance = balance - $1 WHERE user_id = $2", amount, user_id, ) .execute(&mut *tx) .await?; sqlx::query!( "INSERT INTO transactions (user_id, amount, type) VALUES ($1, $2, $3)", user_id, amount, "withdrawal", ) .execute(&mut *tx) .await?; // Commit (or automatic rollback on error) tx.commit().await?; Ok(()) } ``` ## Caching Pattern ### Redis Integration ```rust use redis::AsyncCommands; async fn get_cached_account( state: &AppState, pubkey: &Pubkey, ) -> Result<AccountInfo> { let key = format!("account:{}", pubkey); // Try cache first let mut conn = state.redis.get_async_connection().await .map_err(|e| AppError::Internal(format!("Redis: {}", e)))?; if let Some(cached): Option<Vec<u8>> = conn.get(&key).await.ok().flatten() { return Ok(bincode::deserialize(&cached)?); } // Fetch from Solana (blocking!) let solana_client = state.solana_client.clone(); let pk = *pubkey; let account = tokio::task::spawn_blocking(move || { solana_client.get_account(&pk) }) .await .map_err(|e| AppError::Internal(e.to_string()))? .map_err(AppError::Solana)?; // Cache for 60 seconds let serialized = bincode::serialize(&account)?; let _: () = conn.set_ex(&key, serialized, 60).await .map_err(|e| AppError::Internal(format!("Redis set: {}", e)))?; Ok(account) } ``` ## WebSocket Pattern ### Real-Time Account Updates ```rust use axum::extract::ws::{WebSocket, WebSocketUpgrade}; use futures::{sink::SinkExt, stream::StreamExt}; async fn ws_handler( ws: WebSocketUpgrade, State(state): State<AppState>, ) -> impl IntoResponse { ws.on_upgrade(|socket| handle_socket(socket, state)) } async fn handle_socket(mut socket: WebSocket, state: AppState) { // Subscribe to Solana account updates let pubkey = /* from message */; loop { tokio::select! { // Receive from client Some(msg) = socket.recv() => { match msg { Ok(msg) => { // Handle client message } Err(_) => break, } } // Send Solana updates to client Some(update) = get_solana_update(&state, &pubkey) => { if let Err(_) = socket.send(update).await { break; } } } } } ``` ## Transaction Indexer Pattern ```rust async fn index_transactions( db: PgPool, solana_client: Arc<RpcClient>, program_id: Pubkey, ) -> Result<()> { let mut last_signature = None; loop { // Fetch signatures (blocking!) let client = solana_client.clone(); let pid = program_id; let last_sig = last_signature.clone(); let signatures = tokio::task::spawn_blocking(move || { client.get_signatures_for_address_with_config( &pid, GetConfirmedSignaturesForAddress2Config { before: last_sig, limit: Some(100), ..Default::default() }, ) }) .await .map_err(|e| AppError::Internal(e.to_string()))? .map_err(AppError::Solana)?; if signatures.is_empty() { tokio::time::sleep(Duration::from_secs(5)).await; continue; } // Process each transaction for sig_info in &signatures { let tx = fetch_and_parse_transaction(&solana_client, sig_info).await?; store_transaction(&db, &tx).await?; } last_signature = signatures.last().map(|s| s.signature.parse().unwrap()); } } ``` ## Tower Middleware ### Custom Middleware Stack ```rust use tower::ServiceBuilder; use tower_http::{ trace::TraceLayer, compression::CompressionLayer, timeout::TimeoutLayer, cors::CorsLayer, }; use std::time::Duration; let app = Router::new() .route("/api/data", get(handler)) .layer( ServiceBuilder::new() .layer(TimeoutLayer::new(Duration::from_secs(30))) .layer(CompressionLayer::new()) .layer(TraceLayer::new_for_http()) .layer( CorsLayer::new() .allow_origin(tower_http::cors::Any) .allow_methods([Method::GET, Method::POST]) ) ) .with_state(state); ``` ## Testing Patterns ### Integration Tests ```rust #[cfg(test)] mod tests { use super::*; use axum::{ body::Body, http::{Request, StatusCode}, }; use tower::ServiceExt; #[tokio::test] async fn test_get_account() { let app = create_test_app().await; let response = app .oneshot( Request::builder() .uri("/api/accounts/11111111111111111111111111111111") .method("GET") .body(Body::empty()) .unwrap() ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } async fn create_test_app() -> Router { // Setup test database, mock Solana client, etc. let state = AppState { /* test state */ }; create_router(state) } } ``` ## Best Practices Summary 1. **Never block the runtime**: Use `spawn_blocking` for blocking ops 2. **Use compile-time checked queries**: sqlx query macros 3. **Implement IntoResponse**: Clean error handling 4. **Use Tower middleware**: Compression, tracing, timeouts 5. **Connection pooling**: Database and HTTP clients 6. **Instrument with tracing**: All important operations 7. **Cache RPC responses**: Redis for frequently accessed data 8. **Handle graceful shutdown**: Clean up connections and tasks --- **Sources:** - [Axum 0.8.0 Release](https://tokio.rs/blog/2025-01-01-announcing-axum-0-8-0) - [Axum Best Practices 2025](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - [Async Rust Tokio Guide](https://tokio.rs/tokio/tutorial)
No install command available. Check the GitHub repository for manual installation instructions.
git clone https://github.com/solanabr/solana-ai-kit/blob/main/.claude/skills/backend-async.mdCopy the install command above and run it in your terminal.
Launch Claude Code, Cursor, or your preferred AI coding agent.
Use the prompt template or examples below to test the skill.
Adapt the skill to your specific use case and workflow.
Check the GitHub repository or documentation for usage examples.
skills-collection
Take a free 3-minute scan and get personalized AI skill recommendations.
Take free scan