#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 { /* ... */ }4class InvoiceService { /* ... */ }5 6const billingModule = $module({7 name: "billing",8 services: [PaymentService, InvoiceService],9});
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 primitives?: Array<PrimitiveFactoryLike>; // primitive factories to associate5 atoms?: Array<Atom<any>>; // atoms to register in state6 register?: (alepha: Alepha) => void; // custom registration logic7}
#Automatic vs Manual Registration
By default, 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});
If you provide a register function, automatic registration is disabled. You must handle all registration manually:
1const mod = $module({2 name: "my.module",3 services: [A, B, C],4 register: (alepha) => {5 alepha.with(B); // only B is registered6 // A and C are NOT registered unless explicitly added7 },8});
This is useful for conditional registration or specific initialization order.
#Module Dependencies
Modules can 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 12const DatabaseModule = $module({13 name: "database",14 services: [DatabaseService],15});16 17class ServerProvider { /* ... */ }18 19const ServerModule = $module({20 name: "server",21 services: [CoreModule, DatabaseModule, ServerProvider], // this is valid22});23 24const 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, t } from "alepha"; 2 3const billingConfig = $atom({ 4 name: "billing:config", 5 schema: t.object({ 6 currency: t.text({ default: "USD" }), 7 taxRate: t.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// }
Alepha has also a built-in graph visualization tool in alepha/devtools that shows module boundaries and dependencies.