ESC
Type to search...

Dependency Injection

Rapina AppState and dependency injections methods

Rapina uses a type-safe container called AppState to share services across handlers. You register values once at startup — database pools, config structs, HTTP clients — and inject them anywhere via the State<T> extractor.

AppState is backed by a HashMap<TypeId, Arc<dyn Any + Send + Sync>>. Each type gets one slot, keyed by its Rust type identity:

AppState
  TypeId(AppConfig)          -> Arc<AppConfig>
  TypeId(EmailClient)        -> Arc<EmailClient>
  TypeId(DatabaseConnection) -> Arc<DatabaseConnection>
  TypeId(...)                -> Arc<...>

At startup the state is wrapped in an Arc and shared across all requests. Per request, only the Arc is cloned — no data is ever copied.

State

Registering State

Call .state() for each service you want to share. The only requirement is that the type is Send + Sync + 'static — the value is immediately wrapped in Arc, so Clone is not required:

use rapina::prelude::*;

struct AppConfig {
    name: String,
    base_url: String,
}

#[get("/info")]
async fn info(config: State<AppConfig>) -> String {
    config.name.clone()
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    Rapina::new()
        .state(AppConfig {
            name: "my-api".to_string(),
            base_url: "https://api.example.com".to_string(),
        })
        .discover()
        .listen("127.0.0.1:3000")
        .await
}

Multiple State Types

Each type is stored independently. Inject as many as needed per handler:

struct AppConfig { base_url: String }

struct EmailClient { api_key: String }

#[post("/invite")]
async fn send_invite(
    user: CurrentUser,
    config: State<AppConfig>,
    email: State<EmailClient>,
    body: Json<InviteRequest>,
) -> Result<()> {
    let url = format!("{}/invite/{}", config.base_url, body.token);
    email.send(&body.address, &url).await?;
    Ok(())
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    Rapina::new()
        .state(AppConfig { base_url: "https://api.example.com".to_string() })
        .state(EmailClient { api_key: std::env::var("EMAIL_KEY").unwrap() })
        .discover()
        .listen("127.0.0.1:3000")
        .await
}

Mutable Shared State

AppState already wraps every value in Arc, so nothing is copied per-request. When you need to mutate state at runtime — not just read it — use Arc<RwLock<T>> or Arc<Mutex<T>> for interior mutability:

use std::sync::{Arc, RwLock};
use std::collections::HashMap;

struct TodoStore(Arc<RwLock<HashMap<String, Todo>>>);

impl TodoStore {
    fn new() -> Self {
        Self(Arc::new(RwLock::new(HashMap::new())))
    }
}

#[get("/todos")]
async fn list_todos(store: State<TodoStore>) -> Json<Vec<Todo>> {
    let todos = store.0.read().unwrap().values().cloned().collect();
    Json(todos)
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    Rapina::new()
        .state(TodoStore::new())
        .discover()
        .listen("127.0.0.1:3000")
        .await
}

Missing State

If a handler requests State<T> but T was never registered, the request returns 500 Internal Server Error:

State not registered for type 'my_crate::AppConfig'. Did you forget to call .state()?

Overwriting State

Calling .state() twice with the same type silently overwrites the first value — no error or warning is emitted:

Rapina::new()
    .state(AppConfig { name: "first".to_string() })
    .state(AppConfig { name: "second".to_string() }) // "first" is gone

Each type has exactly one slot in the container.


Graceful Shutdown

When the server receives SIGINT or SIGTERM, it stops accepting new connections and waits for in-flight requests to finish. Use .shutdown_timeout() to control how long it waits, and .on_shutdown() to register async cleanup hooks.

Hooks run after connections drain (or the timeout expires), in the order they were registered.

Closing a database pool on shutdown:

use std::time::Duration;

let pool = build_db_pool().await;
let pool_for_shutdown = pool.clone();

Rapina::new()
    .state(pool)
    .shutdown_timeout(Duration::from_secs(30))
    .on_shutdown(move || async move {
        pool_for_shutdown.close().await;
        tracing::info!("database pool closed");
    })
    .discover()
    .listen("127.0.0.1:3000")
    .await

.state(pool) consumes pool by value, so a handle is cloned beforehand. Both point to the same underlying data via Arc — the hook uses pool_for_shutdown to run cleanup before the state is dropped.

Multiple hooks run in order:

Rapina::new()
    .shutdown_timeout(Duration::from_secs(15))
    .on_shutdown(|| async { tracing::info!("step 1: draining queue") })
    .on_shutdown(|| async { tracing::info!("step 2: closing db pool") })
    .on_shutdown(|| async { tracing::info!("step 3: flushing metrics") })
    .discover()
    .listen("127.0.0.1:3000")
    .await

The default timeout is 30 seconds. After the timeout, remaining connections are dropped and shutdown proceeds.


Going Further

  • Database.with_database(), the Db extractor, and migrations
  • Middleware — CORS, rate limiting, compression, caching, and custom middleware
  • Authentication — JWT with .with_auth() and #[public] routes
  • Metrics — Prometheus scraping with .with_metrics()
  • OpenAPI — generated spec with .openapi()
  • WebSocket — real-time push with .with_relay() and the Relay extractor

Complete Example

A production-ready setup combining state, database, auth, middleware, observability, and graceful shutdown:

use rapina::prelude::*;
use rapina::auth::AuthConfig;
use rapina::database::{DatabaseConfig, Db};
use rapina::middleware::{CorsConfig, RateLimitConfig};
use rapina::observability::TracingConfig;
use std::time::Duration;

struct AppConfig {
    app_name: String,
    frontend_url: String,
}

// Public routes — no token required
#[post("/auth/login")]
#[public]
async fn login(body: Json<LoginRequest>) -> Result<Json<TokenResponse>> {
    // validate credentials and issue JWT...
}

#[get("/health")]
#[public]
async fn health() -> &'static str {
    "ok"
}

// Protected routes
#[get("/users")]
async fn list_users(db: Db, _user: CurrentUser) -> Result<Json<Vec<User>>> {
    let users = UserEntity::find().all(db.conn()).await?;
    Ok(Json(users))
}

#[get("/me")]
async fn me(user: CurrentUser, config: State<AppConfig>) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "id": user.id,
        "app": config.app_name,
    }))
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let config = AppConfig {
        app_name: "my-api".to_string(),
        frontend_url: "https://app.example.com".to_string(),
    };

    let frontend_url = config.frontend_url.clone();

    Rapina::new()
        // Observability (init first so all startup logs are captured)
        .with_tracing(TracingConfig::default())
        // Application state
        .state(config)
        // Database
        .with_database(DatabaseConfig::from_env()?).await?
        .run_migrations::<migrations::Migrator>().await?
        // Middleware
        .with_cors(CorsConfig::with_origins(vec![frontend_url]))
        .with_rate_limit(RateLimitConfig::per_minute(200))
        // Auth — #[public] handlers are exempted automatically
        .with_auth(AuthConfig::from_env()?)
        // OpenAPI + metrics
        .openapi("My API", "1.0.0")
        .with_metrics(true)
        // Graceful shutdown
        .shutdown_timeout(Duration::from_secs(30))
        .on_shutdown(|| async {
            tracing::info!("shutting down gracefully");
        })
        // Routes
        .discover()
        .listen("127.0.0.1:3000")
        .await
}