Skip to content

Recipe: Feature Module

Feature modules are the primary extension mechanism in CruzJS. They allow you to register DI bindings, tRPC routers, React Router routes, and event listeners using the @Module decorator and createCruzApp().

Use the @Module decorator to declare everything a feature contributes:

apps/web/src/features/analytics/analytics.module.ts
import { Module } from '@cruzjs/core/di';
import { AnalyticsService } from './analytics.service';
import { AnalyticsTracker } from './analytics-tracker.service';
import { analyticsRouter } from './analytics.router';
import { UserRegisteredEvent } from '@cruzjs/core/auth/events/user-registered.event';
import { trackRegistration } from './listeners/track-registration.listener';
@Module({
providers: [
AnalyticsService,
AnalyticsTracker,
],
trpcRouters: {
analytics: analyticsRouter,
},
events: [
{
event: UserRegisteredEvent,
listener: trackRegistration,
},
],
})
export class AnalyticsModule {}

Register it in createCruzApp():

server.cloudflare.ts
import { createCruzApp } from '@cruzjs/core';
import { CloudflareAdapter } from '@cruzjs/adapter-cloudflare';
import * as schema from './database/schema';
import { AnalyticsModule } from './features/analytics/analytics.module';
export default createCruzApp({
schema,
modules: [AnalyticsModule],
adapter: new CloudflareAdapter(),
pages: () => import('virtual:react-router/server-build'),
});

The @Module decorator declares:

  • providers — Classes to register in the DI container (auto-bound as singletons)
  • trpcRouters — tRPC routers to merge into the application router
  • events — Event-listener pairs to register with the EventEmitterService
  • pageRoutes — React Router page routes contributed by this feature

The framework loads modules in a defined order:

  1. Core modules load first (Auth, Org, Billing, Email, Jobs, Admin, Upload, AI)
  2. User modules load next (your app’s modules, in the order specified in createCruzApp)

Within each module, the lifecycle is:

  1. Module loading@Module providers, routers, and events are collected
  2. Router registration — tRPC routers are merged
  3. Route registration — React Router routes are added
  4. Event registration — Event listeners are attached

For bindings that go beyond simple class registration, use provider objects in @Module:

apps/web/src/features/search/search.module.ts
import { Module } from '@cruzjs/core/di';
import { SearchService } from './search.service';
import { SEARCH_CLIENT } from './tokens';
import { ConfigService } from '@cruzjs/core';
@Module({
providers: [
SearchService,
// Factory-based binding with runtime logic
{
provide: SEARCH_CLIENT,
useFactory: (config: ConfigService) => {
const apiKey = config.get<string>('SEARCH_API_KEY');
if (!apiKey) return new LocalSearchClient();
return new AlgoliaSearchClient(apiKey);
},
inject: [ConfigService],
},
],
trpcRouters: {
search: searchRouter,
},
})
export class SearchModule {}
@Module({
providers: [NotificationService],
events: [
{ event: MemberAddedEvent, listener: sendWelcomeNotification },
{ event: InvitationCreatedEvent, listener: sendInvitationNotification },
{ event: PaymentFailedEvent, listener: alertBillingAdmin },
],
})
export class NotificationModule {}

For larger applications, organize features into separate modules and pass them all to createCruzApp():

server.cloudflare.ts
import { createCruzApp } from '@cruzjs/core';
import { CloudflareAdapter } from '@cruzjs/adapter-cloudflare';
import * as schema from './database/schema';
import { ProjectModule } from './features/projects/project.module';
import { ReportModule } from './features/reports/report.module';
import { NotificationModule } from './features/notifications/notification.module';
import { IntegrationModule } from './features/integrations/integration.module';
export default createCruzApp({
schema,
modules: [ProjectModule, ReportModule, NotificationModule, IntegrationModule],
adapter: new CloudflareAdapter(),
pages: () => import('virtual:react-router/server-build'),
});

Override or extend core services by rebinding tokens in your module:

import { Module } from '@cruzjs/core/di';
import { SessionService } from '@cruzjs/core/auth/session.service';
import { CustomSessionService } from './custom-session.service';
@Module({
providers: [
// Replace the default session service with a custom one
{ provide: SessionService, useClass: CustomSessionService },
],
})
export class AuthExtensionModule {}