Volga
Home
API Docs
GitHub
  • English
  • Русский
Home
API Docs
GitHub
  • English
  • Русский
  • Home
  • Basics

    • Quick Start
    • Route Parameters
    • Query Parameters
    • Route Groups
    • Headers
  • Data Formats

    • Handling JSON
    • Handling Form Data
    • Working with Files
    • Server-Sent Events (SSE)
  • Protocols

    • HTTP/1 and HTTP/2
    • HTTPS
    • WebSockets & WebTransport
  • Advanced

    • Custom Middleware
    • Response Compression
    • Request Decompression
    • Global Error Handling
    • Dependency Injection
    • Tracing & Logging
    • Static Files
    • CORS (Cross-Origin Resource Sharing)
    • Cookies
    • Request cancellation
    • Custom Handling of HEAD, OPTIONS, and TRACE Methods

Dependency Injection

Volga supports robust dependency injection (DI) with three lifetimes: Singleton, Scoped, and Transient. These lifetimes allow you to manage the lifecycle of your dependencies effectively.

If you're not using the full feature set, ensure you enable the di feature in your Cargo.toml:

[dependencies]
volga = { version = "0.4.5", features = ["di"] }

Dependency Lifetimes

Singleton

A Singleton ensures a single instance of a dependency is created and shared for the entire lifetime of your web application. This instance is thread-safe and reused concurrently across threads.

Example: Singleton Dependency

use volga::{App, di::Dc, ok, not_found};
use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
};

#[derive(Clone, Default)]
struct InMemoryCache {
    inner: Arc<Mutex<HashMap<String, String>>>,
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Register a singleton service globally
    app.add_singleton(InMemoryCache::default());

    // Inject the shared cache instance into the route handlers
    app.map_get("/user/{id}", |id: String, cache: Dc<InMemoryCache>| async move {
        let user = cache.inner.lock().unwrap().get(&id);
        match user {
            Some(user) => ok!(user),
            None => not_found!("User not found"),
        }
    });

    app.map_post("/user/{id}/{name}", |id: String, name: String, cache: Dc<InMemoryCache>| async move {
        cache.inner.lock().unwrap().insert(id, name);
        ok!()
    });

    app.run().await
}

In this example:

  • The add_singleton method registers an InMemoryCache instance as a singleton.
  • The Dc<T> extractor provides access to the dependency container, resolving the dependency as needed.
  • The Dc<T> behaves similarly to other Volga extractors, such as Json<T> or Query<T>.

Info

T must Send, Sync and Default if it doesn't depend on anything or if we're using an already created instance.

Scoped

A Scoped dependency creates a new instance for each HTTP request. The instance persists for the duration of the request, ensuring isolation between requests.

Example: Scoped Dependency

use volga::{App, di::Dc, ok, not_found};
use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
};

#[derive(Clone, Default)]
struct InMemoryCache {
    inner: Arc<Mutex<HashMap<String, String>>>,
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Register a scoped service
    app.add_scoped::<InMemoryCache>();

    // Inject a request-specific cache instance
    app.map_get("/user/{id}", |id: String, cache: Dc<InMemoryCache>| async move {
        let user = cache.inner.lock().unwrap().get(&id);
        match user {
            Some(user) => ok!(user),
            None => not_found!("User not found"),
        }
    });

    app.map_post("/user/{id}/{name}", |id: String, name: String, cache: Dc<InMemoryCache>| async move {
        cache.inner.lock().unwrap().insert(id, name);
        ok!()
    });

    app.run().await
}

Key differences from Singleton:

  • The add_scoped::<T>() method registers a dependency that is instantiated lazily for each request.
  • Each request gets its own, unique instance of InMemoryCache.

Transient

A Transient dependency creates a new instance whenever requested, regardless of the request or context. You can register a transient service using the add_transient::<T>() method. The behavior is similar to Scoped, but a new instance is created on every injection request.

Tips

By implementing Default manually, you can control the instantiation behavior for Scoped and Transient services. For the more advanced scenarios, especially when you need to construct your service by injecting other dependencies you need to use the Inject trait.

DI in middleware

If you need to request a dependency in middleware, use either resolve::<T>() or resolve_shared::<T> methods of HttpContext. The main difference between them is that the first one requires to implement the Clone trait for T while the latter returns an Arc<T>.

app.use_middleware(|ctx: HttpContext, next: Next| async move {
    let cache = ctx.resolve::<InMemoryCache>()?;
    // do something....
    next(ctx).await
});

Summary

  • Singleton: Shared instance across the entire application lifecycle.
  • Scoped: New instance for each HTTP request.
  • Transient: New instance for every injection request.

For more advanced examples, check out the this.

Last Updated:
Prev
Global Error Handling
Next
Tracing & Logging