Skip to content

createModule()

Creates a typed module factory for organizing domain handlers.

Basic Usage

typescript
import { createModule } from '@eventflows/core';

const accountModule = createModule({
  name: 'accounts',
  setup: ({ eventStore }) => {
    const accountRepository = new AccountRepository(eventStore);
    return {
      commandHandlers: {
        CreateAccount: new CreateAccountHandler(accountRepository),
        DepositMoney: new DepositMoneyHandler(accountRepository),
      },
    };
  },
});

Module Structure

The createModule() function accepts a configuration object with:

PropertyTypeDescription
namestringUnique module identifier
setupfunctionFactory function receiving dependencies

The setup function receives { eventStore, eventBus } and returns:

PropertyTypeDescription
commandHandlersRecord<string, ICommandHandler>Command handlers keyed by command name
queryHandlersRecord<string, IQueryHandler>Query handlers keyed by query name
eventHandlersRecord<string, EventHandler[]>Event handlers keyed by event name

With Command Handlers

typescript
interface CreateAccountCommand extends ICommand {
  commandName: 'CreateAccount';
  accountId: string;
  initialBalance: number;
}

class CreateAccountHandler implements ICommandHandler<CreateAccountCommand, void> {
  constructor(private readonly repository: AccountRepository) {}

  async execute(command: CreateAccountCommand): Promise<void> {
    const account = Account.create(command.accountId, command.initialBalance);
    await this.repository.save(account);
  }
}

const accountModule = createModule({
  name: 'accounts',
  setup: ({ eventStore }) => {
    const repository = new AccountRepository(eventStore);
    return {
      commandHandlers: {
        CreateAccount: new CreateAccountHandler(repository),
      },
    };
  },
});

With Query Handlers

typescript
interface GetAccountBalanceQuery extends IQuery {
  queryName: 'GetAccountBalance';
  accountId: string;
}

class GetAccountBalanceHandler implements IQueryHandler<GetAccountBalanceQuery, number> {
  constructor(private readonly readRepository: AccountBalanceReadRepository) {}

  async execute(query: GetAccountBalanceQuery): Promise<number> {
    return await this.readRepository.getBalance(query.accountId);
  }
}

const accountModule = createModule({
  name: 'accounts',
  setup: ({ eventStore }) => {
    const writeRepository = new AccountRepository(eventStore);
    const readRepository = new AccountBalanceReadRepository();
    return {
      commandHandlers: {
        CreateAccount: new CreateAccountHandler(writeRepository),
      },
      queryHandlers: {
        GetAccountBalance: new GetAccountBalanceHandler(readRepository),
      },
    };
  },
});

With Event Handlers

Event handlers react to domain events. Multiple handlers can subscribe to the same event:

typescript
class AccountBalanceProjection {
  constructor(private readonly readRepository: AccountBalanceReadRepository) {}

  async onMoneyDeposited(event: EventEnvelope<MoneyDeposited>): Promise<void> {
    await this.readRepository.updateBalance(
      event.metadata.aggregateId,
      event.payload.amount
    );
  }
}

const accountModule = createModule({
  name: 'accounts',
  setup: ({ eventStore, eventBus }) => {
    const writeRepository = new AccountRepository(eventStore);
    const readRepository = new AccountBalanceReadRepository();
    const projection = new AccountBalanceProjection(readRepository);

    return {
      commandHandlers: {
        CreateAccount: new CreateAccountHandler(writeRepository),
        DepositMoney: new DepositMoneyHandler(writeRepository),
      },
      queryHandlers: {
        GetAccountBalance: new GetAccountBalanceHandler(readRepository),
      },
      eventHandlers: {
        MoneyDeposited: [
          (event) => projection.onMoneyDeposited(event),
          (event) => console.log('Deposit:', event.payload.amount),
        ],
      },
    };
  },
});

Type Inference

Handler names are preserved as string literal types, enabling intellisense:

typescript
const module = createModule({
  name: 'users',
  setup: ({ eventStore }) => ({
    commandHandlers: {
      CreateUser: new CreateUserHandler(),  // Type: 'CreateUser'
      UpdateUser: new UpdateUserHandler(),  // Type: 'UpdateUser'
    },
  }),
});

// When used with createEventFlowsApp(), these become:
// app.commands.CreateUser({ ... })  - Full type inference
// app.commands.UpdateUser({ ... })  - Full type inference
typescript
const orderModule = createModule({
  name: 'orders',
  setup: ({ eventStore, eventBus }) => {
    // Create repositories with injected dependencies
    const orderRepository = new OrderRepository(eventStore);
    const orderReadRepository = new InMemoryOrderReadRepository();
    const orderProjection = new OrderProjection(orderReadRepository);

    return {
      commandHandlers: {
        PlaceOrder: new PlaceOrderHandler(orderRepository),
        CancelOrder: new CancelOrderHandler(orderRepository),
        ShipOrder: new ShipOrderHandler(orderRepository),
      },
      queryHandlers: {
        GetOrderById: new GetOrderByIdHandler(orderReadRepository),
        ListOrdersByCustomer: new ListOrdersByCustomerHandler(orderReadRepository),
      },
      eventHandlers: {
        OrderPlaced: [
          (event) => orderProjection.onOrderPlaced(event),
          (event) => notifyWarehouse(event),
        ],
        OrderShipped: [
          (event) => orderProjection.onOrderShipped(event),
          (event) => notifyCustomer(event),
        ],
      },
    };
  },
});

HTTP Routes

Modules can optionally define HTTP routes to expose their handlers as REST endpoints. Routes are declared as a top-level routes property alongside name and setup:

typescript
import { z } from 'zod';

const createOrderSchema = z.object({
  customerId: z.string(),
  items: z.array(z.object({ sku: z.string(), quantity: z.number() })),
});

const orderModule = createModule({
  name: 'orders',
  setup: ({ eventStore }) => ({
    commandHandlers: {
      PlaceOrder: new PlaceOrderHandler(new OrderRepository(eventStore)),
    },
    queryHandlers: {
      GetOrderById: new GetOrderByIdHandler(),
    },
  }),
  routes: {
    basePath: '/orders',
    commands: {
      PlaceOrder: { method: 'POST', path: '/', schema: createOrderSchema },
    },
    queries: {
      GetOrderById: { method: 'GET', path: '/:orderId' },
    },
  },
});

Routes are metadata only -- they describe how handlers should be exposed over HTTP but do not change the module's runtime behavior. Modules without routes continue to work exactly as before. The route metadata is consumed by createHttpServer() to generate endpoints automatically.

See HTTP Integration for full details on defining routes and creating an HTTP server.

Best Practices

  • One Module Per Bounded Context: Align modules with domain boundaries
  • Handler Naming: Use clear, action-based names (CreateUser, not User)
  • Dependency Injection: Create dependencies inside setup() using provided infrastructure
  • Immutability: Modules are frozen after creation
  • Event Handlers for Side Effects: Use event handlers for cross-cutting concerns

See implementation in packages/core/src/module/create-module.ts

Released under the MIT License.