A compile-time dependency injection framework for Rust, inspired by Axum's typed extractor model.
Current docs target injectable on Rust 1.86+.
- Repository: https://github.com/jymchng/injectable
- Guide index: guides/README.md
- AI skills index: skills/README.md
use injectable::prelude::*;
#[injectable]
#[derive(Default)]
struct Database;
#[injectable]
struct UserService {
db: Inject<Database>,
}
impl UserService {
fn get_user(&self, id: u32) -> String { format!("User #{id}") }
}
#[tokio::main]
async fn main() {
let container = Container::builder().build().await.unwrap();
let svc = container.resolve::<UserService>().await.unwrap();
println!("{}", svc.get_user(1));
}No runtime reflection. Dependency chains are encoded into generated Provider impls at compile time. The resolved type is always statically known — no TypeId lookups, no Box<dyn Any> in the hot path.
Axum-compatible. Inject<T> implements FromRequestParts, so dependencies drop into Axum handler signatures exactly like Query<T> or Path<T>.
Fail early. Circular dependencies, missing registrations, and scope mismatches are caught at Container::builder().build() — before any request is served.
You can still test without the container. Constructors are plain Rust functions. Call them directly in unit tests with test doubles.
[dependencies]
injectable = { version = "0.2", features = ["axum"] }
tokio = { version = "1", features = ["full"] }Import everything via the prelude:
use injectable::prelude::*;injectable has a small set of building blocks, but they combine in several
important ways. The table below is the shortest way to choose the right one.
| Situation | Recommended pattern | Typical syntax |
|---|---|---|
| Your type is owned by your crate and all fields are injectable | Field injection | #[injectable] struct Svc { dep: Inject<Db> } |
| Your type needs custom construction logic | Constructor injection | #[injectable] impl Svc { #[injectable(ctor)] fn new(...) -> Self } |
| You need a shared app dependency | Shared wrapper | #[injectable] struct DbPool { ... } |
| You need a third-party type only inside one service | Field or ctor factory | #[injectable(inject(use_factory_async = path))] |
| You need a third-party type registered centrally | Dynamic provider | DynProvider::sync/new/with_ctx/from_value |
| A dependency is optional | Optional injection | Option<Inject<T>> |
| You want trait-object injection | Trait binding | bind!(dyn Trait => Concrete) + Inject<dyn Trait> |
| You want per-resolution instances | Scope marker | #[injectable(scope = Transient)] |
| You want Axum handler injection | Extractor integration | Inject<UserService> in handler params |
Use #[injectable] on:
- a
structfor field injection - an
implblock for constructor injection
use injectable::prelude::*;
#[injectable]
#[derive(Default)]
pub struct Database;
#[injectable]
#[derive(Default)]
pub struct Cache;
#[injectable]
pub struct UserRepository {
db: Inject<Database>,
cache: Inject<Cache>,
}For reqwest::Client, sqlx::SqlitePool, and other third-party types, use one
of these patterns:
| Pattern | Best for | Example |
|---|---|---|
#[injectable(factory)] |
Reusable injectable-aware factory function | #[injectable(factory)] async fn make_pool(cfg: Inject<AppConfig>) -> ... |
use_factory_async |
Async field or constructor parameter creation | #[injectable(inject(use_factory_async = self::make_pool))] |
use_factory_sync |
Sync field or constructor parameter creation | #[injectable(inject(use_factory_sync = self::make_client))] |
DynProvider::sync |
Central synchronous registration | `.register(DynProvider::sync( |
DynProvider::new |
Central async registration without context | `.register(DynProvider::new( |
DynProvider::with_ctx |
Central async registration with injectable deps | `.register(DynProvider::with_ctx( |
DynProvider::from_value |
Tests, feature flags, fixed values | .register(DynProvider::from_value(mock_client)) |
Example:
let container = Container::builder()
.register(DynProvider::sync(|| Ok(reqwest::Client::new())))
.register(DynProvider::new(|| async {
Ok(sqlx::SqlitePool::connect("sqlite:./app.db").await?)
}))
.register(DynProvider::with_ctx(|ctx| async move {
let config: Inject<AppConfig> = ctx.extract().await?;
Ok(sqlx::SqlitePool::connect(&config.database_url).await?)
}))
.build()
.await?;Inside DynProvider::with_ctx, prefer ctx.extract::<Inject<T>>() for
injectable types. Use ctx.resolve_external::<T>() for other
DynProvider-registered types.
Field injection is the lowest-boilerplate path when your service can be constructed directly from its dependencies.
#[injectable]
pub struct UserService {
db: Inject<Database>,
#[injectable(inject)]
cache: Arc<Cache>,
}Constructor injection is the right choice when:
- you need to set plain scalar fields manually
- you need validation or transformation inside
new() - you want the public constructor shape to tell the story
pub struct EmailService {
pool: sqlx::SqlitePool,
config: Arc<AppConfig>,
retry: u32,
}
#[injectable]
impl EmailService {
#[injectable(ctor)]
pub async fn new(
#[injectable(inject(use_factory_async = self::make_pool))] pool: sqlx::SqlitePool,
#[injectable(inject)] config: Arc<AppConfig>,
) -> Self {
Self { pool, config, retry: 3 }
}
}Real applications usually mix both styles:
- config and wrappers via constructor injection
- service layers via field injection
- external leaves via factories
- handlers via Axum extractors
See 17-multi-service-web-app-patterns.md.
The same small set of rules applies to both fields and constructor parameters.
| Declared type | Annotation | Meaning |
|---|---|---|
Inject<T> |
none | Shared injectable dependency |
Arc<T> |
#[injectable(inject)] |
Singleton Arc<T> for T: Injectable |
T |
#[injectable(inject)] |
Owned clone of singleton T when T: Clone + Injectable |
Option<Inject<T>> |
#[injectable(inject)] on fields when needed |
Optional injectable dependency |
Inject<dyn Trait> |
none | Trait-object dependency after bind!() |
Option<Inject<dyn Trait>> |
#[injectable(inject)] on fields when needed |
Optional trait binding |
External T |
#[injectable(inject(use_factory_async = path))] |
Async factory-backed external dependency |
External T |
#[injectable(inject(use_factory_sync = path))] |
Sync factory-backed external dependency |
Practical rule:
- use
Inject<T>by default - use
Arc<T>when you want to store a plainArc - use
Tonly when cloning the singleton value is actually what you want - use a wrapper type when several services must share one external resource
Use this when you want a reusable factory function whose parameters are injectable extractors:
#[injectable(factory)]
async fn make_db_pool(cfg: Inject<AppConfig>) -> Result<sqlx::Pool<sqlx::Sqlite>, sqlx::Error> {
sqlx::SqlitePool::connect(&cfg.database_url).await
}Use these when the consuming field or constructor parameter should directly own the produced value:
async fn make_pool(ctx: &ResolveContext) -> Result<sqlx::SqlitePool, sqlx::Error> {
let cfg: Inject<AppConfig> = ctx.extract().await?;
sqlx::SqlitePool::connect(&cfg.database_url).await
}
fn make_client(_ctx: &ResolveContext) -> reqwest::Client {
reqwest::Client::new()
}| Need | Prefer |
|---|---|
| Function args written as injectable types | #[injectable(factory)] |
Direct ctx.extract() access |
plain fn/async fn(&ResolveContext) |
| One external instance shared across many services | wrapper service + factory |
| One external value local to a single service | direct use_factory_async/sync |
Important: use_factory_async on multiple services is not, by itself, a
cross-service singleton. If several services must share one pool, client, or
socket, wrap it in your own injectable type and inject that wrapper.
This is the recommended pattern for cross-service sharing of third-party types:
#[injectable(factory)]
async fn make_db_pool(cfg: Inject<AppConfig>) -> Result<sqlx::Pool<sqlx::Sqlite>, sqlx::Error> {
sqlx::SqlitePool::connect(&cfg.database_url).await
}
pub struct DbPool {
pool: sqlx::Pool<sqlx::Sqlite>,
}
impl Clone for DbPool {
fn clone(&self) -> Self {
Self { pool: self.pool.clone() }
}
}
#[injectable]
impl DbPool {
#[injectable(ctor)]
fn new(
#[injectable(inject(use_factory_async = make_db_pool))] pool: sqlx::Pool<sqlx::Sqlite>,
) -> Self {
Self { pool }
}
}
#[injectable]
pub struct UserService {
db: Inject<DbPool>,
}
#[injectable]
pub struct AuditService {
#[injectable(inject)]
db: Arc<DbPool>,
}Inject<DbPool> and #[injectable(inject)] Arc<DbPool> share the same
singleton wrapper instance.
Trait-object injection is supported through bind!():
#[injectable(trait)]
trait EmailSender: Send + Sync {
fn send(&self, to: &str) -> String;
}
#[injectable]
#[derive(Default, Clone)]
struct SmtpSender;
impl EmailSender for SmtpSender {
fn send(&self, to: &str) -> String {
format!("sent to {to}")
}
}
bind!(dyn EmailSender => SmtpSender);
#[injectable]
struct NotificationService {
sender: Inject<dyn EmailSender>,
}You can also use:
#[injectable(inject)] sender: Option<Inject<dyn EmailSender>>- constructor params of type
Inject<dyn Trait>
Optional dependencies are modeled with Option<Inject<T>>:
#[injectable]
pub struct Notifier {
#[injectable(inject)]
sms: Option<Inject<SmsClient>>,
}
impl Notifier {
pub fn send(&self, msg: &str) {
if let Some(s) = &self.sms {
s.send(msg);
}
}
}This pattern works well for:
- feature-gated registrations
- local development without all infrastructure
- tests that replace production dependencies selectively
Scope markers are type-safe and live under the same #[injectable(...)]
surface:
| Scope | Syntax | Behavior |
|---|---|---|
| Singleton | default or #[injectable(scope = Singleton)] |
One shared instance |
| Transient | #[injectable(scope = Transient)] |
New instance on every resolution |
| Request-scoped | #[injectable(scope = RequestScoped)] |
Scoped to a request context |
Use singleton for long-lived services, transient for per-use workers, and request scope when a dependency should be isolated to one request lifecycle.
Hooks are supported on #[injectable] impl blocks:
use injectable::prelude::*;
pub struct ConnectionPool { /* ... */ }
impl Clone for ConnectionPool {
fn clone(&self) -> Self { /* ... */ }
}
#[injectable]
impl ConnectionPool {
#[injectable(ctor)]
pub fn new() -> Self { /* ... */ }
#[injectable(post_construct)]
pub async fn warm_up(&self) -> HookResult {
Ok(())
}
#[injectable(pre_destruct)]
pub async fn drain(&self) -> HookResult {
Ok(())
}
}Rules:
post_constructruns after constructionpre_destructruns duringcontainer.shutdown().awaitpre_destructexamples should make the typeClone
There are several valid places to resolve dependencies:
| API | Use when |
|---|---|
container.resolve::<T>().await |
Resolving an injectable type |
container.resolve_external::<T>().await |
Resolving a DynProvider-registered external type |
ctx.extract::<Inject<T>>().await |
Resolving inside factories or custom code with scope-safe semantics |
Inject<T> in Axum handlers |
Resolving directly from request state |
Example:
let service: UserService = container.resolve().await?;
let client: reqwest::Client = container.resolve_external().await?;
let ctx = container.context();
let db: Inject<Database> = ctx.extract().await?;Enable the axum feature and inject services directly into handlers:
use axum::{Json, Router, extract::Path, routing::get};
use injectable::axum::AxumState;
use injectable::prelude::*;
async fn get_user(
Path(id): Path<u64>,
Inject(svc): Inject<UserService>,
) -> Json<User> {
Json(svc.get(id).await.unwrap())
}
let state = AxumState::new(container);
let app = Router::new()
.route("/users/:id", get(get_user))
.with_state(state);You can also provide your own state type by implementing
injectable::axum::InjectableState.
Inject<T> wraps Arc<T>, implements Deref<Target = T>, and is the default
way to express shared dependencies.
let svc: Inject<UserService> = container.resolve().await?;
svc.some_method();
let arc = svc.arc();
let arc = svc.into_inner();
let Inject(arc) = svc;Use Inject<T> when:
- the dependency is logically shared
- you want the most ergonomic default
- you want the same type to work in services, constructors, and Axum handlers
Container::builder().build().await
│
├── Collect GraphNode entries
├── Validate duplicate nodes
├── Validate missing dependencies
├── Validate cycles
├── Validate scope mismatches
└── Build ResolveContext → Container
Typical failures:
dependency graph validation failed:
- circular dependency detected: OrderService -> UserService -> OrderService
- `InvoiceService` depends on `PdfRenderer`, which is not registered
| Flag | Description |
|---|---|
axum |
Inject<T>: FromRequestParts, AxumState, InjectableRejection |
See guides/README.md for a categorized guide index and contributor-oriented release/documentation notes.
| Resource | Link |
|---|---|
| Homepage | https://github.com/jymchng/injectable |
| Repository | https://github.com/jymchng/injectable |
| Development + release guide | guides/16-development-and-release.md |
| AI skills catalog | skills/README.md |
# Basic field injection
cargo run --example 01_basic_field_injection
# Constructor injection patterns
cargo run --example 02_constructor_injection
# External types (reqwest::Client, sqlx::SqlitePool)
cargo run --example 03_external_types
# Lifecycle hooks: post_construct + pre_destruct
cargo run --example 04_lifecycle_hooks
# Dependency graph inspection
cargo run --example 05_dependency_graph
# Scopes
cargo run --example 06_scopes
# Axum integration (requires axum feature)
cargo run --example 07_axum_integration --features axum
# Realistic web app: config + sqlx + services + axum
cargo run --example 08_realistic_web_app --features axum
# Weather API with sqlx + reqwest + axum
cargo run --example 09_weather_api --features axum
# Multi-service weather + users app
cargo run --example 10_weather_users_api --features axum
# URL shortener with full CRUD
cargo run --example 11_url_shortener --features axumMIT OR Apache-2.0
