Contributor
Dependency Injection
Factory functions, explicit wiring, and why Toast avoids runtime DI containers and global singletons.
Toast uses factory functions with explicit dependency injection.
That means:
- repositories receive
db - services receive repositories and infrastructure via
deps - routes receive services
- the runtime is wired explicitly in
container.ts,index.ts, andapp.ts
There is no DI container, no decorators, and no module-level getDb() / getAuth() pattern in the active runtime.
Why the old singleton model hurt
The earlier pattern relied on hidden module-level dependencies:
import { getDb } from '@toast/drizzle';
export async function createContent(input) {
const db = getDb();
// ...
}That caused three problems:
- Implicit ordering — database/auth initialization order was enforced only by convention
- Hard-to-test services — tests depended on
vi.mock()and module import behavior - Opaque runtime graph — it was hard to see what actually depended on what
The current pattern
Repository
export function createContentRepository(db: PostgresJsDatabase) {
return {
findBySiteId(siteId: string) {
return db.select().from(content).where(eq(content.siteId, siteId));
},
};
}Service
export function createContentService(deps: {
contentRepository: ContentRepository;
eventBus: EventBus;
}) {
return {
async createContent(input: CreateContentInput, siteId: string) {
const row = await deps.contentRepository.create({ ...input, siteId });
deps.eventBus.emit(/* domain event */);
return row;
},
};
}Route factory
export function createContentRoutes(deps: { contentService: ContentService }) {
const controller = createContentController(deps.contentService);
const routes = new OpenAPIHono<ApiEnv>();
// routes.openapi(...)
return routes;
}Where the wiring happens now
apps/api/src/container.ts
buildInfrastructure(config)creates logger, database, and event busbuildStacks(...)creates auth, repositories, and services
apps/api/src/index.ts
- creates concrete route instances from the route factories
- registers subscribers like
createAuditLogSubscriber(...).register() - passes the assembled routes into
createApp()
apps/api/src/app.ts
- mounts the route instances
- configures middleware and docs endpoints
This is the current runtime shape. container.ts is central, but it is not the only top-level file involved.
What this buys us
Easier testing
Service tests can use plain object mocks:
const service = createContentService({
contentRepository: { create: vi.fn(), findBySiteId: vi.fn() },
eventBus: { emit: vi.fn(), subscribe: vi.fn(), unsubscribeAll: vi.fn() },
});No import-order tricks, no global singleton reset.
Explicit runtime graph
If you want to know how a feature is wired, read:
container.tsroutes/index.tsindex.tsapp.ts
Graceful shutdown
The database connection is owned by infrastructure and closed from index.ts on shutdown.