Dependency Injection¶
Register shared services once and inject them into handlers by name. Value dependencies are singletons; factory dependencies can be request-scoped (cacheable) or singletons.
Register dependencies¶
from spikard import Spikard
from spikard.di import Provide
app = Spikard()
# Value dependency (singleton)
app.provide("config", {"db_url": "postgresql://localhost/app"})
# Factory dependency (depends on config, cached globally)
async def make_pool(config: dict[str, str]):
return {"url": config["db_url"], "client": "pool"}
app.provide("db_pool", Provide(make_pool, depends_on=["config"], singleton=True))
@app.get("/stats")
async def stats(config: dict[str, str], db_pool: dict[str, str]):
return {"db": db_pool["url"], "env": config["db_url"]}
import { Spikard, type Request } from "spikard";
const app = new Spikard();
// Value dependency (singleton)
app.provide("config", { dbUrl: "postgresql://localhost/app" });
// Factory dependency (depends on config, singleton)
app.provide(
"dbPool",
async ({ config }) => {
// connect using config.dbUrl
return { url: config.dbUrl, driver: "pool" };
},
{ dependsOn: ["config"], singleton: true },
);
app.addRoute(
{ method: "GET", path: "/stats", handler_name: "stats", is_async: true },
async (req: Request) => {
const deps = req.dependencies ?? {};
return { db: deps.dbPool?.url, env: deps.config?.dbUrl };
},
);
require "spikard"
app = Spikard::App.new
# Value dependency (singleton)
app.provide("config", { "db_url" => "postgresql://localhost/app" })
# Factory dependency (depends on config, singleton)
app.provide("db_pool", depends_on: ["config"], singleton: true) do |config:|
{ url: config["db_url"], client: "pool" }
end
app.get("/stats") do |_params, _query, _body, config:, db_pool:|
{ db: db_pool[:url], env: config["db_url"] }
end
<?php
declare(strict_types=1);
use Spikard\App;
use Spikard\Attributes\Get;
use Spikard\Config\ServerConfig;
use Spikard\DI\DependencyContainer;
use Spikard\DI\Provide;
use Spikard\Http\Response;
// Create dependency container with values and factories
$container = DependencyContainer::builder()
->withValue('config', ['db_url' => 'postgresql://localhost/app'])
->withFactory(
'db_pool',
new Provide(
factory: fn (array $config) => [
'url' => $config['db_url'],
'client' => 'pool'
],
dependsOn: ['config'],
singleton: true
)
)
->build();
final class StatsController
{
#[Get('/stats')]
public function stats(): Response
{
return Response::json(['status' => 'ok', 'db' => 'connected']);
}
}
$app = (new App(new ServerConfig(port: 8000)))
->withDependencies($container)
->registerController(new StatsController());
use axum::response::Json;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use spikard::{get, App, RequestContext};
use spikard_http::ServerConfig;
use std::sync::Arc;
#[derive(Serialize, Deserialize, JsonSchema)]
struct Health {
db: String,
env: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Register dependencies on the server config
let config = ServerConfig::builder()
.provide_value("config", "postgresql://localhost/app".to_string())
.provide_factory("db_pool", |resolved| async move {
let url: Arc<String> = resolved.get("config").ok_or("missing config")?;
Ok(format!("pool({})", url))
})
.build();
let mut app = App::new().config(config);
app.route(get("/stats"), |ctx: RequestContext| async move {
let deps = ctx.dependencies();
let db = deps.and_then(|d| d.get::<String>("db_pool")).cloned().unwrap_or_default();
let env = deps
.and_then(|d| d.get::<String>("config"))
.cloned()
.unwrap_or_else(|| "unknown".to_string());
Ok(Json(Health { db, env }).into())
})?;
app.run().await?;
Ok(())
}
Notes¶
- Value dependencies are cached globally. Use factories for per-request values and set
cacheable/use_cachewhen you need a fresh value each time. - Factories can depend on other dependencies; unresolved or circular graphs fail fast with clear errors.
- Cleanup generators (Python) and singleton toggles (TypeScript/Ruby) mirror the core DI engine semantics.