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 goneEach 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")
.awaitThe default timeout is 30 seconds. After the timeout, remaining connections are dropped and shutdown proceeds.
Going Further
- Database —
.with_database(), theDbextractor, 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 theRelayextractor
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
}