alepha@docs:~/docs/framework/guides/core$
cat 4-modules.md | pretty
3 min read
Last commit:

#Modules

$module groups related services into named, self-contained units. It helps organize large applications into domain-driven bounded contexts.

#When to Use Modules

Do not use modules for small applications. They add structure that only pays off at scale.

A reasonable guideline: introduce modules when you have more than 30 actions in a single codebase. An application with 100 actions should have at least 3 modules.

It's also highly recommended in full-stack mode to make 2 modules: api (server) and web (client). The api module contains all server-side services and actions. The web module contains all client-side services (e.g. React components, hooks, etc). This keeps server and client code separate and prevents accidental imports of server-only code into the client.

#Basic Usage

typescript
 1import { $module } from "alepha"; 2  3class PaymentService { 4  /* ... */ 5} 6class InvoiceService { 7  /* ... */ 8} 9 10const billingModule = $module({11  name: "billing",12  services: [PaymentService, InvoiceService],13});

Register the module with the container:

typescript
1const alepha = Alepha.create().with(billingModule);

All services listed in services are automatically instantiated and registered in the container when the module is loaded.

#Module Names

Module names must follow the pattern project.module.submodule - lowercase letters, hyphens, and dots:

txt
core                    // valid
my.app                  // valid
my.app.billing          // valid
my-app.billing          // valid

The regex: /^[a-z-]+(\.[a-z-][a-z0-9-]*)*$/

Module names are used in logging. Each service in a module has its logger prefixed with the module name:

txt
[23:45:53.326] INFO <billing.PaymentService>: Processing payment

This enables per-module log level configuration:

bash
LOG_LEVEL=billing:debug,info

#Module Options

typescript
1interface ModulePrimitiveOptions {2  name: string; // required3  services?: Array<Service>; // services to register4  imports?: Array<Service<Module>>; // other modules this one depends on5  variants?: Array<Service>; // opt-in services (not auto-registered)6  primitives?: Array<PrimitiveFactoryLike>; // primitive factories to associate7  atoms?: Array<Atom<any>>; // atoms to register in state8  register?: (alepha: Alepha) => void; // extra registration logic9}

#Registration Order

All services in the services array are instantiated automatically:

typescript
1const mod = $module({2  name: "my.module",3  services: [A, B, C], // all three are registered4});

A register function adds custom logic - conditional providers, atom seeding, environment checks - but it never suppresses auto-registration: services are always injected. The ordering guarantee is: atoms are registered, then register() runs, then imports are wired, then services are injected - so substitutions recorded in register() (e.g. alepha.with({ provide, use })) apply to the subsequent auto-injection.

typescript
1const mod = $module({2  name: "my.module",3  services: [A, B, C],4  register: (alepha) => {5    if (process.env.FEATURE_X) {6      alepha.with({ provide: A, use: SpecialA });7    }8  },9});

Services listed in variants are not auto-registered - they're opt-in implementations the user wires explicitly with alepha.with(...) (e.g. a transport choice).

#Module Dependencies

Declare dependencies on other modules with imports - preferred over nesting modules in services:

typescript
1const ServerModule = $module({2  name: "server",3  imports: [CoreModule, DatabaseModule],4  services: [ServerProvider],5});

Modules can also contain other modules in their services array:

typescript
 1class RandomService { 2  very = $inject(VeryRandomService); 3} 4  5const CoreModule = $module({ 6  name: "core", 7  services: [RandomService, VeryRandomService], 8}); 9 10class DatabaseService {11  /* ... */12}13 14const DatabaseModule = $module({15  name: "database",16  services: [DatabaseService],17});18 19class ServerProvider {20  /* ... */21}22 23const ServerModule = $module({24  name: "server",25  services: [CoreModule, DatabaseModule, ServerProvider], // this is valid26});27 28const alepha = Alepha.create().with(ServerModule);

Each service retains its own module context. RandomService belongs to "core", DatabaseService belongs to "database", and ServerProvider belongs to "server".

#Auto-Discovery

If a service has a [MODULE] association (set by $module), injecting that service anywhere will automatically load its parent module:

typescript
 1const billingModule = $module({ 2  name: "billing", 3  services: [PaymentService], 4}); 5  6// In another service, just inject PaymentService directly. 7// The billing module is loaded automatically. 8class OrderService { 9  payments = $inject(PaymentService);10}

There is no need to explicitly register billingModule if something already depends on one of its services.

#Registering Atoms

Modules can register atoms in their state:

typescript
 1import { $atom, $module, z } from "alepha"; 2  3const billingConfig = $atom({ 4  name: "billing.config", 5  schema: z.object({ 6    currency: z.text({ default: "USD" }), 7    taxRate: z.number().default(0.2), 8  }), 9  default: { currency: "USD", taxRate: 0.2 },10});11 12const billingModule = $module({13  name: "billing",14  services: [PaymentService],15  atoms: [billingConfig],16});

#Dependency Graph

Inspect the dependency graph to see module associations:

typescript
1const alepha = Alepha.create().with(ServerModule);2console.log(alepha.graph());3// {4//   RandomService: { from: ["core"], module: "core" },5//   DatabaseService: { from: ["database"], module: "database" },6//   ServerProvider: { from: ["server"], module: "server" },7//   ...8// }

The devtools plugin (@alepha/devtools) also includes a graph visualization that shows module boundaries and dependencies.