Toast
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, and app.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:

  1. Implicit ordering — database/auth initialization order was enforced only by convention
  2. Hard-to-test services — tests depended on vi.mock() and module import behavior
  3. 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 bus
  • buildStacks(...) 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:

  1. container.ts
  2. routes/index.ts
  3. index.ts
  4. app.ts

Graceful shutdown

The database connection is owned by infrastructure and closed from index.ts on shutdown.


On this page