Bootstrap Pattern

Wiring dependencies together with a layered bootstrap architecture.

Overview

The bootstrap pattern orchestrates dependency injection across layers. It creates instances in the correct order: adapters → use cases → routes.

bootstrap/
├── index.ts                 # Main orchestration
├── adapters.bootstrap.ts    # Infrastructure adapters
└── use-cases.bootstrap.ts   # Application use cases

The unified route system handles the presentation layer wiring automatically through handlers.


Main Orchestration

The entry point creates all dependencies in order:

// bootstrap/index.ts
import { createAdapters } from './adapters.bootstrap';
import { createUseCases } from './use-cases.bootstrap';
import { createProjectManagementRoutes } from '../presentation/http/handlers';

export function bootstrapProjectManagement() {
  // 1. Create adapters (infrastructure layer)
  const adapters = createAdapters();

  // 2. Create use cases with adapters (application layer)
  const useCases = createUseCases(adapters);

  // 3. Create routes with handlers (presentation layer)
  const routes = createProjectManagementRoutes(useCases);

  return { adapters, useCases, routes };
}

export type ProjectManagementModule = ReturnType<typeof bootstrapProjectManagement>;

Adapters Bootstrap

Creates infrastructure adapters (repositories, external services):

// bootstrap/adapters.bootstrap.ts
import { ProjectRepository } from '../infra/persistence/project.repository';
import { ProjectQueryRepository } from '../infra/persistence/project-query.repository';

export function createAdapters() {
  return {
    projectRepository: new ProjectRepository(),
    projectQueryRepository: new ProjectQueryRepository(),
  };
}

export type Adapters = ReturnType<typeof createAdapters>;

Use Cases Bootstrap

Creates use cases with injected adapters:

// bootstrap/use-cases.bootstrap.ts
import type { Adapters } from './adapters.bootstrap';
import { CreateProjectUseCase } from '../app/use-cases/create-project.command';
import { GetProjectQuery } from '../app/use-cases/get-project.query';
import { ListProjectsQuery } from '../app/use-cases/list-projects.query';

export function createUseCases(adapters: Adapters) {
  return {
    createProjectUseCase: new CreateProjectUseCase(adapters.projectRepository),
    getProjectQuery: new GetProjectQuery(adapters.projectQueryRepository),
    listProjectsQuery: new ListProjectsQuery(adapters.projectQueryRepository),
  };
}

export type UseCases = ReturnType<typeof createUseCases>;

Routes (Handlers)

Create handlers using the serverRoutes() builder:

// presentation/http/handlers/index.ts
import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';
import { projectManagementRouter } from '../router';
import type { UseCases } from '../../../bootstrap/use-cases.bootstrap';

export function createProjectManagementRoutes(useCases: UseCases) {
  return serverRoutes(projectManagementRouter)
    .handle('projects.create', {
      requestMapper: (req, ctx) => ({
        name: req.body.name,
        description: req.body.description,
        createdBy: ctx.userId,
      }),
      useCase: useCases.createProjectUseCase,
      responseMapper: (output) => ({
        status: 201 as const,
        body: { projectId: output.projectId },
      }),
    })
    .handle('projects.list', {
      requestMapper: (req) => ({
        page: req.query?.page ?? 1,
        pageSize: req.query?.pageSize ?? 20,
      }),
      useCase: useCases.listProjectsQuery,
      responseMapper: (output) => ({
        status: 200 as const,
        body: output,
      }),
    })
    .handle('projects.get', {
      requestMapper: (req) => ({
        projectId: req.pathParams.projectId,
      }),
      useCase: useCases.getProjectQuery,
      responseMapper: (output) => ({
        status: 200 as const,
        body: output,
      }),
    })
    .build();
}

The serverRoutes() builder:

  • Validates requests against route schemas automatically
  • Provides full type inference for req and ctx parameters
  • Handles error conversion to HTTP responses

Using the Bootstrap

In your application entry point:

// src/index.ts
import { Hono } from 'hono';
import { registerHonoRoutes, onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/hono';
import { bootstrapProjectManagement } from '@repo/backend/bounded-contexts/project-management';

// Bootstrap the bounded context
const { routes } = bootstrapProjectManagement();

// Create Hono app
const app = new Hono();

// Error handler
app.onError((err, c) => onionErrorHandler(err, c));

// Register routes with middleware and context extraction
registerHonoRoutes(app, routes, {
  middlewares: [authMiddleware],
  contextExtractor: (c) => ({
    userId: c.get('jwtPayload')?.sub,
  }),
});

export default app;

Directory Structure

The bootstrap folder should be at the root of your bounded context:

bounded-contexts/project-management/
├── bootstrap/                    # Dependency wiring
│   ├── index.ts                  # Main orchestration
│   ├── adapters.bootstrap.ts     # Infrastructure adapters
│   └── use-cases.bootstrap.ts    # Application use cases
├── app/                          # Application layer
│   ├── ports/                    # Use case interfaces
│   └── use-cases/                # Use case implementations
├── domain/                       # Domain layer
│   ├── entities/
│   ├── value-objects/
│   └── events/
├── infra/                        # Infrastructure layer
│   └── persistence/              # Repository implementations
└── presentation/                 # Presentation layer
    └── http/
        ├── routes/               # Route definitions
        ├── handlers/             # Route handlers
        └── router.ts             # Combined router

Splitting Handlers

For large bounded contexts, split handlers into separate files:

// presentation/http/handlers/projects.handlers.ts
export function createProjectHandlers(useCases: UseCases) {
  return serverRoutes(projectManagementRouter)
    .handle('projects.create', { ... })
    .handle('projects.list', { ... })
    .buildPartial();  // Returns partial routes
}

// presentation/http/handlers/tasks.handlers.ts
export function createTaskHandlers(useCases: UseCases) {
  return serverRoutes(projectManagementRouter)
    .handle('projects.tasks.add', { ... })
    .handle('projects.tasks.list', { ... })
    .buildPartial();
}

// presentation/http/handlers/index.ts
export function createProjectManagementRoutes(useCases: UseCases) {
  return [
    ...createProjectHandlers(useCases),
    ...createTaskHandlers(useCases),
  ];
}

Benefits

  • Clear dependency flow: Adapters → Use Cases → Routes
  • Simplified wiring: No separate controller or validator bootstrap needed
  • Type safety: Full type inference from route definitions to handlers
  • Testability: Each layer can be tested independently with mocks
  • Single entry point: bootstrapProjectManagement() returns everything needed