Building Rust Services with ScoutQuest
Learn how to build high-performance Rust services with ScoutQuest service discovery. This tutorial covers service registration, discovery, and communication patterns using Rust.
Prerequisites
- Rust 1.70+ installed
- Cargo package manager
- ScoutQuest server running
- Basic knowledge of Rust and async programming
Project Setup
Create a new Rust project and add ScoutQuest dependencies:
Create new project
cargo new --bin rust-service-example
cd rust-service-example
Cargo.toml
[package]
name = "rust-service-example"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
axum = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
scoutquest-rust = "1.0"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0"
Basic Rust Service
Let's build a user service using Axum web framework:
src/main.rs
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use scoutquest_rust::ServiceDiscoveryClient;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
use tower_http::cors::CorsLayer;
use tracing::{info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
type UserStore = Arc>>;
#[derive(Clone)]
struct AppState {
users: UserStore,
scout_client: ServiceDiscoveryClient,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize tracing
tracing_subscriber::init();
// Initialize ScoutQuest client
let scout_client = ServiceDiscoveryClient::new("http://localhost:8080", None)?;
// Create application state
let state = AppState {
users: Arc::new(Mutex::new(create_sample_users())),
scout_client,
};
// Build our application with routes
let app = Router::new()
.route("/health", get(health_check))
.route("/users", get(get_users).post(create_user))
.route("/users/:id", get(get_user))
.route("/users/:id/orders", get(get_user_orders))
.layer(CorsLayer::permissive())
.with_state(state.clone());
// Start the server
let port = std::env::var("PORT")
.unwrap_or_else(|_| "3001".to_string())
.parse::()?;
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
info!("🚀 User service starting on port {}", port);
// Register with ScoutQuest
tokio::spawn(register_service(state.scout_client.clone(), port));
// Start serving
axum::serve(listener, app).await?;
Ok(())
}
async fn register_service(client: ServiceDiscoveryClient, port: u16) {
match client.register_service(
"user-service",
"localhost",
port,
Some(vec!["api".to_string(), "users".to_string(), "v1".to_string()]),
Some([("version".to_string(), "1.0.0".to_string())].into()),
).await {
Ok(_) => info!("✅ Registered with ScoutQuest"),
Err(e) => warn!("❌ Failed to register with ScoutQuest: {}", e),
}
}
fn create_sample_users() -> HashMap {
let mut users = HashMap::new();
users.insert(1, User {
id: 1,
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
});
users.insert(2, User {
id: 2,
name: "Jane Smith".to_string(),
email: "jane@example.com".to_string(),
});
users
}
async fn health_check() -> Json {
Json(serde_json::json!({
"status": "healthy",
"service": "user-service",
"timestamp": chrono::Utc::now().to_rfc3339()
}))
}
async fn get_users(State(state): State) -> Json> {
let users = state.users.lock().await;
let user_list: Vec = users.values().cloned().collect();
Json(user_list)
}
async fn get_user(
Path(id): Path,
State(state): State,
) -> Result, StatusCode> {
let users = state.users.lock().await;
match users.get(&id) {
Some(user) => Ok(Json(user.clone())),
None => Err(StatusCode::NOT_FOUND),
}
}
async fn create_user(
State(state): State,
Json(payload): Json,
) -> Result, StatusCode> {
let mut users = state.users.lock().await;
let id = users.len() as u32 + 1;
let user = User {
id,
name: payload.name,
email: payload.email,
};
users.insert(id, user.clone());
Ok(Json(user))
}
async fn get_user_orders(
Path(user_id): Path,
State(state): State,
) -> Result, StatusCode> {
// Check if user exists
let users = state.users.lock().await;
if !users.contains_key(&user_id) {
return Err(StatusCode::NOT_FOUND);
}
drop(users);
// Call order service using ScoutQuest
match state.scout_client.get::(
"order-service",
&format!("/orders/user/{}", user_id)
).await {
Ok(orders) => Ok(Json(orders)),
Err(_) => Err(StatusCode::SERVICE_UNAVAILABLE),
}
}
Service Communication
Create a service that communicates with other services:
Order Service Example
use axum::{extract::State, response::Json, routing::post, Router};
use scoutquest_rust::ServiceDiscoveryClient;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Serialize, Deserialize)]
struct Order {
id: u32,
user_id: u32,
product_id: u32,
quantity: u32,
total: f64,
status: String,
}
#[derive(Deserialize)]
struct CreateOrder {
user_id: u32,
product_id: u32,
quantity: u32,
}
#[derive(Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Deserialize)]
struct Product {
id: u32,
name: String,
price: f64,
stock: u32,
}
async fn create_order(
State(client): State>,
Json(payload): Json,
) -> Result, axum::http::StatusCode> {
// Verify user exists
let user: User = match client.get("user-service", &format!("/users/{}", payload.user_id)).await {
Ok(user) => user,
Err(_) => return Err(axum::http::StatusCode::BAD_REQUEST),
};
// Verify product exists and has sufficient stock
let product: Product = match client.get("product-service", &format!("/products/{}", payload.product_id)).await {
Ok(product) => product,
Err(_) => return Err(axum::http::StatusCode::BAD_REQUEST),
};
if product.stock < payload.quantity {
return Err(axum::http::StatusCode::BAD_REQUEST);
}
// Update product stock
let updated_stock = serde_json::json!({ "stock": product.stock - payload.quantity });
if let Err(_) = client.put("product-service", &format!("/products/{}/stock", payload.product_id), &updated_stock).await {
return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
// Create order
let order = Order {
id: 1, // In real app, use proper ID generation
user_id: payload.user_id,
product_id: payload.product_id,
quantity: payload.quantity,
total: product.price * payload.quantity as f64,
status: "confirmed".to_string(),
};
Ok(Json(order))
}
Error Handling and Resilience
Implement proper error handling and resilience patterns:
Custom Error Types
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Service not found: {0}")]
ServiceNotFound(String),
#[error("Service unavailable: {0}")]
ServiceUnavailable(String),
#[error("Invalid request: {0}")]
BadRequest(String),
#[error("Internal error: {0}")]
Internal(String),
}
impl IntoResponse for ServiceError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
ServiceError::ServiceNotFound(msg) => (StatusCode::NOT_FOUND, msg),
ServiceError::ServiceUnavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg),
ServiceError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
ServiceError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
};
let body = Json(json!({
"error": error_message,
"status": status.as_u16()
}));
(status, body).into_response()
}
}
Retry Logic
use std::time::Duration;
use tokio::time::sleep;
pub struct RetryConfig {
pub max_attempts: u32,
pub base_delay: Duration,
pub max_delay: Duration,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
base_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(5),
}
}
}
pub async fn with_retry(
operation: F,
config: RetryConfig,
) -> Result
where
F: Fn() -> Fut,
Fut: std::future::Future
Health Monitoring
Implement comprehensive health checks:
Advanced Health Check
use serde_json::{json, Value};
use std::collections::HashMap;
#[derive(Debug)]
pub struct HealthCheck {
name: String,
check_fn: Box std::pin::Pin + Send>> + Send + Sync>,
}
impl HealthCheck {
pub fn new(name: String, check_fn: F) -> Self
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future
Testing Rust Services
Write comprehensive tests for your Rust services:
tests/integration_tests.rs
use axum::{body::Body, http::{Request, StatusCode}};
use scoutquest_rust::ServiceDiscoveryClient;
use serde_json::json;
use tower::ServiceExt;
#[tokio::test]
async fn test_user_service_integration() {
// Setup test environment
let scout_client = ServiceDiscoveryClient::new("http://localhost:8080", None)
.expect("Failed to create ScoutQuest client");
let app_state = AppState {
users: Arc::new(Mutex::new(create_sample_users())),
scout_client,
};
let app = create_app(app_state);
// Test health check
let response = app
.clone()
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
// Test get users
let response = app
.clone()
.oneshot(Request::builder().uri("/users").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
// Test create user
let new_user = json!({
"name": "Test User",
"email": "test@example.com"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/users")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&new_user).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_service_discovery() {
let client = ServiceDiscoveryClient::new("http://localhost:8080", None)
.expect("Failed to create client");
// Register test service
let result = client
.register_service(
"test-service",
"localhost",
3999,
Some(vec!["test".to_string()]),
None,
)
.await;
assert!(result.is_ok());
// List services
let services = client.list_services().await.expect("Failed to list services");
assert!(services.iter().any(|s| s.name == "test-service"));
// Cleanup
let _ = client.deregister_service("test-service").await;
}
Running the Service
Build and run your Rust service:
Development
# Run in development mode with auto-reload
cargo install cargo-watch
cargo watch -x run
# Or just run once
cargo run
Production Build
# Build optimized release
cargo build --release
# Run the optimized binary
./target/release/rust-service-example
Environment Configuration
# Set environment variables
export SCOUT_URL=http://localhost:8080
export PORT=3001
export RUST_LOG=info
cargo run
Performance Considerations
- Connection Pooling: Reuse HTTP connections for service calls
- Async/Await: Use Tokio for high-concurrency applications
- Memory Management: Leverage Rust's zero-cost abstractions
- Caching: Cache service discovery results when appropriate
- Metrics: Use libraries like `metrics` for observability
Next Steps
- Deploy your service with Docker containers
- Set up monitoring and observability
- Implement advanced health checking
- Explore the full microservices tutorial