One command for Claude Code, Cursor, Codex, Windsurf and other agents — installs from the upstream repo (current HEAD; it ships 6 skills — pick backend-async when prompted).
Drop the mirrored SKILL.md into your skills directory. Global (all projects):
Or scoped to the current project:
The skill is mirrored as plain markdown — tell your agent:
Or have the skills CLI generate a one-shot prompt (pipe it straight into your agent):
Or pull the raw file into a terminal session:
Copy the skill directory straight from GitHub:
Skill contents
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
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+)
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
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
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
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!
// ❌ 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
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
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
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
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
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
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
#[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
- Never block the runtime: Use
spawn_blockingfor blocking ops - Use compile-time checked queries: sqlx query macros
- Implement IntoResponse: Clean error handling
- Use Tower middleware: Compression, tracing, timeouts
- Connection pooling: Database and HTTP clients
- Instrument with tracing: All important operations
- Cache RPC responses: Redis for frequently accessed data
- Handle graceful shutdown: Clean up connections and tasks
Sources: