Claude Code Plugins

Community-maintained marketplace

Feedback
0
0

Expert knowledge for building web APIs and real-time applications with Composable Rust. Use when building HTTP REST APIs with Axum, implementing WebSocket for real-time updates, working with authentication (magic link, OAuth, passkeys), setting up email providers (SMTP, Console), designing API routes or request handlers, or questions about web integration and real-time communication.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name composable-rust-web
description Expert knowledge for building web APIs and real-time applications with Composable Rust. Use when building HTTP REST APIs with Axum, implementing WebSocket for real-time updates, working with authentication (magic link, OAuth, passkeys), setting up email providers (SMTP, Console), designing API routes or request handlers, or questions about web integration and real-time communication.

Composable Rust Web Integration Expert

Expert knowledge for building web APIs and real-time applications with Composable Rust - HTTP/WebSocket patterns, authentication framework, action broadcasting, email providers, and client integration.

When to Use This Skill

Automatically apply when:

  • Building HTTP REST APIs with Axum
  • Implementing WebSocket for real-time updates
  • Working with authentication (magic link, OAuth, passkeys)
  • Setting up email providers (SMTP, Console)
  • Designing API routes or request handlers
  • Questions about web integration, real-time communication, or auth

HTTP API Pattern with Axum

Basic Router Setup

use axum::{
    Router,
    routing::{get, post},
    extract::State,
    Json,
};

pub fn create_router(store: AppStore) -> Router {
    Router::new()
        .route("/health", get(health_check))
        .route("/api/v1/orders", post(create_order))
        .route("/api/v1/orders/:id", get(get_order))
        .with_state(store)
}

async fn health_check() -> &'static str {
    "OK"
}

Pattern: Share store via State. Use versioned routes (/api/v1/). Include health check.

Request Handler Pattern

#[derive(Debug, Deserialize)]
pub struct CreateOrderRequest {
    pub customer_id: String,
    pub items: Vec<Item>,
}

#[derive(Debug, Serialize)]
pub struct CreateOrderResponse {
    pub order_id: String,
    pub status: String,
}

async fn create_order(
    State(store): State<Arc<Store<OrderState, OrderAction, OrderEnvironment, OrderReducer>>>,
    Json(request): Json<CreateOrderRequest>,
) -> Result<Json<CreateOrderResponse>, ApiError> {
    // Convert request to action
    let action = OrderAction::PlaceOrder {
        customer_id: request.customer_id,
        items: request.items,
    };

    // Send action to store and wait for result
    let result = store
        .send_and_wait_for(
            action,
            |action| matches!(action, OrderAction::OrderPlaced { .. }),
            Duration::from_secs(5),
        )
        .await?;

    // Extract result
    let order_id = match result {
        OrderAction::OrderPlaced { order_id, .. } => order_id,
        _ => return Err(ApiError::UnexpectedResponse),
    };

    Ok(Json(CreateOrderResponse {
        order_id,
        status: "placed".to_string(),
    }))
}

Pattern:

  1. Deserialize request to DTO
  2. Convert DTO to Action
  3. Use send_and_wait_for for request-response
  4. Extract result from action
  5. Serialize response DTO

Error Handling Pattern

#[derive(Debug)]
pub enum ApiError {
    NotFound,
    BadRequest(String),
    InternalServerError(String),
    Timeout,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ApiError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
            ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.leak()),
            ApiError::InternalServerError(msg) => {
                tracing::error!("Internal server error: {}", msg);
                (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
            }
            ApiError::Timeout => (StatusCode::REQUEST_TIMEOUT, "Request timeout"),
        };

        (status, message).into_response()
    }
}

// Conversion from domain errors
impl From<DomainError> for ApiError {
    fn from(err: DomainError) -> Self {
        match err {
            DomainError::NotFound => ApiError::NotFound,
            DomainError::ValidationError(msg) => ApiError::BadRequest(msg),
            _ => ApiError::InternalServerError(err.to_string()),
        }
    }
}

Pattern: Define API-specific error type. Implement IntoResponse. Convert domain errors. Log internal errors.

WebSocket Pattern (Real-Time Updates)

WebSocket Handler Setup

use axum::extract::ws::{WebSocket, WebSocketUpgrade};
use tokio::sync::broadcast;

pub fn create_router_with_websocket(
    store: AppStore,
    action_tx: broadcast::Sender<Action>,
) -> Router {
    Router::new()
        .route("/ws", get(websocket_handler))
        .with_state((store, action_tx))
}

async fn websocket_handler(
    ws: WebSocketUpgrade,
    State((store, action_tx)): State<(AppStore, broadcast::Sender<Action>)>,
) -> impl IntoResponse {
    ws.on_upgrade(|socket| handle_socket(socket, store, action_tx))
}

Pattern: Use broadcast channel for action broadcasting. Share both store and channel.

Message Protocol (JSON Envelope)

WebSocket messages use a tagged envelope format with a type field:

Message Types:

Type Direction Purpose
command Client → Server Execute an action
event Server → Client Broadcast an action
error Server → Client Error occurred
ping Bidirectional Keep connection alive
pong Bidirectional Respond to ping

Command (Client → Server):

{
  "type": "command",
  "action": {
    "PlaceOrder": {
      "customer_id": "cust-123",
      "items": [{"product_id": "prod-1", "quantity": 2}]
    }
  }
}

Event (Server → Client):

{
  "type": "event",
  "action": {
    "OrderPlaced": {
      "order_id": "ord-456",
      "customer_id": "cust-123",
      "status": "pending"
    }
  }
}

Error (Server → Client):

{
  "type": "error",
  "message": "Invalid action format: missing customer_id"
}

Keep-Alive (Bidirectional):

{"type": "ping"}
{"type": "pong"}

Important: Clients receive events for ALL actions from effects, not just their own commands.

Socket Handler Pattern

async fn handle_socket(
    mut socket: WebSocket,
    store: AppStore,
    action_tx: broadcast::Sender<Action>,
) {
    let mut action_rx = action_tx.subscribe();

    // Spawn task to broadcast actions to this client
    let mut send_task = tokio::spawn(async move {
        while let Ok(action) = action_rx.recv().await {
            // Filter actions to send (optional)
            if should_broadcast(&action) {
                let message = serde_json::to_string(&action).unwrap();
                if socket.send(Message::Text(message)).await.is_err() {
                    break;
                }
            }
        }
    });

    // Spawn task to receive messages from client
    let mut recv_task = tokio::spawn(async move {
        while let Some(Ok(Message::Text(text))) = socket.recv().await {
            if let Ok(action) = serde_json::from_str::<Action>(&text) {
                // Client sent action, send to store
                let _ = store.send(action).await;
            }
        }
    });

    // Wait for either task to finish
    tokio::select! {
        _ = (&mut send_task) => recv_task.abort(),
        _ = (&mut recv_task) => send_task.abort(),
    }
}

Pattern:

  1. Subscribe to action broadcast
  2. Spawn send task (broadcasts actions to client)
  3. Spawn receive task (receives actions from client)
  4. Use tokio::select! to handle disconnection

Action Broadcasting Pattern

In your Store, broadcast actions after executing effects:

pub async fn send(&self, action: Action) {
    // Reduce
    let effects = self.reducer.reduce(&mut state, action.clone(), &self.env);

    // Broadcast action to WebSocket clients
    let _ = self.action_tx.send(action);

    // Execute effects
    for effect in effects {
        self.execute_effect(effect).await;
    }
}

Pattern: Broadcast action before or after reducing (depending on needs). Ignore send errors (no subscribers OK).

Streaming Actions via WebSocket (Phase 8)

Effect::Stream automatically broadcasts each streamed action to WebSocket clients:

// In your reducer
fn reduce(...) -> SmallVec<[Effect<Action>; 4]> {
    match action {
        Action::StartLlmGeneration { prompt } => {
            // Stream LLM tokens as they arrive
            let effect = Effect::Stream(Box::pin(async_stream::stream! {
                let mut response_stream = env.llm_client.messages_stream(prompt).await?;

                while let Some(chunk) = response_stream.next().await {
                    // Each yield is automatically broadcast to WebSocket clients
                    yield Action::LlmChunk {
                        content: chunk?.delta.text
                    };
                }

                yield Action::LlmComplete;
            }));

            smallvec![effect]
        }
        // ...
    }
}

Result: WebSocket clients receive LlmChunk actions in real-time as tokens arrive.

Use cases:

  • LLM token streaming (ChatGPT-style UX)
  • Progress updates from long-running operations
  • Multi-step workflow status broadcasts
  • Real-time data feed processing

Client Integration (React Example)

// useWebSocket hook
function useWebSocket(url: string) {
  const [actions, setActions] = useState<Action[]>([]);
  const ws = useRef<WebSocket | null>(null);

  useEffect(() => {
    ws.current = new WebSocket(url);

    ws.current.onmessage = (event) => {
      const action = JSON.parse(event.data);
      setActions((prev) => [...prev, action]);
    };

    return () => ws.current?.close();
  }, [url]);

  const sendAction = (action: Action) => {
    ws.current?.send(JSON.stringify(action));
  };

  return { actions, sendAction };
}

// Usage in component
function OrderList() {
  const { actions, sendAction } = useWebSocket('ws://localhost:3000/ws');

  // Filter order actions
  const orderActions = actions.filter(
    (a) => a.type === 'OrderPlaced' || a.type === 'OrderUpdated'
  );

  return (
    <div>
      {orderActions.map((action) => (
        <div key={action.order_id}>{/* Render order */}</div>
      ))}
    </div>
  );
}

Pattern: Single WebSocket connection. Filter actions client-side. Update local state on action received.

Filtering Actions for Broadcast

fn should_broadcast(action: &OrderAction) -> bool {
    match action {
        // Broadcast external-facing events
        OrderAction::OrderPlaced { .. } => true,
        OrderAction::OrderCancelled { .. } => true,

        // Don't broadcast internal actions
        OrderAction::InternalStateUpdate { .. } => false,

        _ => true,
    }
}

Pattern: Only broadcast actions that clients care about. Reduces bandwidth and client processing.

Authentication Framework

Auth Crate Integration

use composable_rust_auth::{
    AuthConfig, AuthStore, MagicLinkProvider, SmtpEmailProvider,
};

// Setup auth store
let email_provider = SmtpEmailProvider::new(smtp_config)?;
let auth_config = AuthConfig::builder()
    .magic_link_enabled(true)
    .oauth_providers(vec!["google", "github"])
    .build();

let auth_store = AuthStore::new(
    AuthState::default(),
    AuthReducer::new(auth_config),
    AuthEnvironment {
        database: postgres_db,
        email_provider,
        clock: SystemClock,
    },
);

// Add auth routes
let app = Router::new()
    .merge(auth_routes(auth_store.clone()))
    .route("/api/v1/protected", get(protected_handler))
    .layer(AuthLayer::new(auth_store.clone()));

Auth Routes Pattern

pub fn auth_routes(auth_store: AuthStore) -> Router {
    Router::new()
        .route("/auth/magic-link/request", post(request_magic_link))
        .route("/auth/magic-link/verify", post(verify_magic_link))
        .route("/auth/oauth/:provider", get(oauth_redirect))
        .route("/auth/oauth/:provider/callback", get(oauth_callback))
        .route("/auth/logout", post(logout))
        .with_state(auth_store)
}

#[derive(Deserialize)]
struct MagicLinkRequest {
    email: String,
}

async fn request_magic_link(
    State(auth_store): State<AuthStore>,
    Json(req): Json<MagicLinkRequest>,
) -> Result<StatusCode, ApiError> {
    auth_store
        .send(AuthAction::RequestMagicLink { email: req.email })
        .await?;

    Ok(StatusCode::OK)
}

Protected Route Pattern

use axum::middleware::from_fn_with_state;

async fn auth_middleware(
    State(auth_store): State<AuthStore>,
    mut request: Request<Body>,
    next: Next,
) -> Result<Response, ApiError> {
    // Extract token from Authorization header
    let token = request
        .headers()
        .get(AUTHORIZATION)
        .and_then(|h| h.to_str().ok())
        .and_then(|s| s.strip_prefix("Bearer "))
        .ok_or(ApiError::Unauthorized)?;

    // Verify token
    let user_id = auth_store
        .send_and_wait_for(
            AuthAction::VerifyToken { token: token.to_string() },
            |action| matches!(action, AuthAction::TokenVerified { .. }),
            Duration::from_secs(2),
        )
        .await?;

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

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

// Protected handler
async fn protected_handler(
    Extension(user_id): Extension<UserId>,
) -> Result<String, ApiError> {
    Ok(format!("Hello, user {}", user_id))
}

Pattern: Middleware extracts token. Verifies via auth store. Adds user ID to extensions. Handler extracts user ID.

Magic Link Flow

1. Client → POST /auth/magic-link/request { email }
2. Server → Send email with link containing token
3. Client → GET /auth/magic-link/verify?token=xxx
4. Server → Verify token, return session
5. Client → Store session, use for authenticated requests

OAuth Flow

1. Client → GET /auth/oauth/google
2. Server → Redirect to Google OAuth
3. User → Authorizes on Google
4. Google → Redirect to /auth/oauth/google/callback?code=xxx
5. Server → Exchange code for tokens, create session, redirect to app
6. Client → Extract session, use for authenticated requests

Email Providers

SMTP Provider Pattern

use composable_rust_email::{EmailProvider, SmtpEmailProvider, Email};

let smtp_provider = SmtpEmailProvider::builder()
    .host("smtp.gmail.com")
    .port(587)
    .username(env::var("SMTP_USERNAME")?)
    .password(env::var("SMTP_PASSWORD")?)
    .from_email("noreply@example.com")
    .from_name("Example App")
    .build()?;

// Send email via effect
vec![Effect::SendEmail {
    provider: smtp_provider,
    email: Email {
        to: "user@example.com".to_string(),
        subject: "Magic link".to_string(),
        body: format!("Click here: {}", magic_link_url),
    },
}]

Console Provider Pattern (Development)

use composable_rust_email::ConsoleEmailProvider;

let console_provider = ConsoleEmailProvider::new();

// Prints email to console instead of sending
// Useful for local development

Email in Reducer Pattern

fn reduce(&self, state: &mut State, action: Action, env: &Env) -> Vec<Effect> {
    match action {
        AuthAction::RequestMagicLink { email } => {
            // Generate token
            let token = generate_token();
            let magic_link = format!("https://app.com/auth/verify?token={}", token);

            // Store token
            state.pending_tokens.insert(token.clone(), email.clone());

            // Send email
            vec![Effect::SendEmail {
                email: Email {
                    to: email,
                    subject: "Your magic link".to_string(),
                    body: format!("Click here to sign in: {}", magic_link),
                },
            }]
        }
        _ => vec![Effect::None],
    }
}

Pattern: Generate token in reducer (deterministic). Store in state. Return Effect::SendEmail.

CORS and Middleware

CORS Setup

use tower_http::cors::{CorsLayer, Any};

let cors = CorsLayer::new()
    .allow_origin(Any)  // For development
    .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
    .allow_headers([CONTENT_TYPE, AUTHORIZATION]);

let app = Router::new()
    .route("/api/v1/orders", post(create_order))
    .layer(cors);

Production: Use .allow_origin(Origin::exact("https://app.com")) instead of Any.

Logging Middleware

use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/api/v1/orders", post(create_order))
    .layer(TraceLayer::new_for_http());

Request-Response Pattern

send_and_wait_for

Most common pattern for HTTP handlers:

async fn handler(
    State(store): State<AppStore>,
    Json(req): Json<CreateRequest>,
) -> Result<Json<Response>, ApiError> {
    let action = Action::from_request(req);

    // Wait for specific response action
    let result = store
        .send_and_wait_for(
            action,
            |action| matches!(action, Action::Success { .. }),
            Duration::from_secs(5),
        )
        .await?;

    Ok(Json(Response::from_action(result)))
}

Pattern: Send action. Wait for result action matching predicate. Timeout after duration.

Async Effect with Callback

Alternative: Use effect that returns result action:

use composable_rust_core::async_effect;

fn reduce(...) -> Vec<Effect> {
    vec![async_effect! {
        let result = database.save(&data).await?;

        // Return action with result
        Some(Action::SaveComplete {
            success: true,
            id: result.id,
        })
    }]
}

Pattern: Effect returns Option<Action>. Store feeds it back into reducer.

Security Patterns

Input Validation

#[derive(Deserialize)]
struct CreateOrderRequest {
    #[serde(deserialize_with = "validate_email")]
    customer_email: String,

    #[serde(deserialize_with = "validate_items")]
    items: Vec<Item>,
}

fn validate_email<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let email = String::deserialize(deserializer)?;
    if email.contains('@') && email.len() > 3 {
        Ok(email)
    } else {
        Err(serde::de::Error::custom("Invalid email"))
    }
}

Pattern: Validate during deserialization. Reject invalid data early.

Rate Limiting

use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer};

let governor_conf = GovernorConfigBuilder::default()
    .per_second(10)  // 10 requests per second
    .burst_size(20)  // Allow burst of 20
    .finish()
    .unwrap();

let app = Router::new()
    .route("/api/v1/orders", post(create_order))
    .layer(GovernorLayer { config: governor_conf });

SQL Injection Prevention

Always use parameterized queries via sqlx:

// ✅ SAFE - Parameterized query
sqlx::query("SELECT * FROM orders WHERE customer_id = $1")
    .bind(&customer_id)
    .fetch_all(&pool)
    .await?;

// ❌ UNSAFE - String interpolation
sqlx::query(&format!("SELECT * FROM orders WHERE customer_id = '{}'", customer_id))
    .fetch_all(&pool)
    .await?;

Common Anti-Patterns to Avoid

❌ Anti-Pattern 1: Blocking Operations in Handlers

// ❌ Synchronous blocking call
async fn handler(State(store): State<AppStore>) -> Result<Response, Error> {
    std::thread::sleep(Duration::from_secs(5));  // ❌ Blocks executor
}

Solution: Use async operations. If sync needed, use spawn_blocking.

❌ Anti-Pattern 2: Not Using send_and_wait_for

// ❌ Fire-and-forget, no result
async fn create_order(State(store): State<AppStore>, Json(req): Json<Request>) -> Response {
    store.send(Action::Create { ... }).await;
    // ❌ No way to know if it succeeded
    Response::new("OK".into())
}

Solution: Use send_and_wait_for to get result.

❌ Anti-Pattern 3: Broadcasting All Actions

// ❌ Broadcasting internal state updates
self.action_tx.send(Action::InternalDebugLog { ... });

Solution: Filter actions before broadcasting.

❌ Anti-Pattern 4: Mixing Business Logic in Handlers

// ❌ Business logic in handler
async fn create_order(State(store): State<AppStore>, Json(req): Json<Request>) -> Response {
    // ❌ Validation, calculation in handler
    if req.items.is_empty() {
        return Response::error("No items");
    }
    let total = req.items.iter().map(|i| i.price).sum();

    // Then send to store
}

Solution: Put all business logic in reducer. Handler only translates HTTP ↔ Actions.

❌ Anti-Pattern 5: Not Handling WebSocket Disconnects

// ❌ No error handling
while let Some(Ok(msg)) = socket.recv().await {
    // Process message
}  // ❌ Loop exits silently on error

Solution: Log disconnects, clean up resources.

Testing Patterns

Unit Test: Request Handler Logic

#[tokio::test]
async fn test_create_order_success() {
    let store = test_store();

    let request = CreateOrderRequest {
        customer_id: "cust-123".to_string(),
        items: vec![test_item()],
    };

    let response = create_order(State(store), Json(request)).await.unwrap();

    assert_eq!(response.status, "placed");
    assert!(response.order_id.starts_with("order-"));
}

Integration Test: HTTP API

#[tokio::test]
async fn test_http_api() {
    let app = create_router(test_store());
    let server = TestServer::new(app).unwrap();

    let response = server
        .post("/api/v1/orders")
        .json(&json!({
            "customer_id": "cust-123",
            "items": [{"id": "item-1", "quantity": 2}]
        }))
        .await;

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

    let body: CreateOrderResponse = response.json().await;
    assert!(!body.order_id.is_empty());
}

Integration Test: WebSocket

#[tokio::test]
async fn test_websocket_broadcast() {
    let (action_tx, _) = broadcast::channel(100);
    let app = create_router_with_websocket(test_store(), action_tx.clone());

    let mut client = test_websocket_client(app).await;

    // Send action via broadcast
    action_tx.send(OrderAction::OrderPlaced { order_id: "123".to_string() }).unwrap();

    // Receive action on client
    let message = client.recv().await.unwrap();
    let action: OrderAction = serde_json::from_str(&message).unwrap();

    assert!(matches!(action, OrderAction::OrderPlaced { .. }));
}

Performance Considerations

  • WebSocket connections: ~1MB RAM per connection, scale to 10k+ per instance
  • HTTP throughput: Axum handles 100k+ req/sec on modern hardware
  • Action broadcasting: broadcast channel is efficient, low overhead
  • send_and_wait_for latency: Adds ~1-10ms depending on reducer complexity

Quick Reference Checklist

When building web APIs:

  • Versioned routes: Use /api/v1/ prefix
  • Health check: Include /health endpoint
  • Error handling: Convert domain errors to HTTP errors
  • CORS: Configure for production
  • Authentication: Use middleware for protected routes
  • Rate limiting: Protect against abuse
  • Input validation: Validate in deserialization or reducer
  • WebSocket filtering: Only broadcast relevant actions
  • Logging: Use TraceLayer for request logging
  • Timeouts: Use send_and_wait_for with reasonable timeout

See Also

  • Architecture: composable-rust-architecture.skill - Core patterns
  • Authentication: docs/authentication.md - Complete auth guide
  • WebSocket: docs/websocket.md - WebSocket patterns
  • Email: docs/email-providers.md - Email setup

Remember: HTTP handlers translate between HTTP and Actions. Business logic stays in reducers. WebSocket broadcasts actions to connected clients. Authentication via middleware.