#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
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:
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:
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:
[23:45:53.326] INFO <billing.PaymentService>: Processing payment
This enables per-module log level configuration:
LOG_LEVEL=billing:debug,info
#Module Options
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:
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.
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:
1const ServerModule = $module({2 name: "server",3 imports: [CoreModule, DatabaseModule],4 services: [ServerProvider],5});
Modules can also contain other modules in their services array:
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:
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:
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:
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.