alepha@docs:~/docs/guides/core$
cat 4-modules.md
2 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 { /* ... */ }4class InvoiceService { /* ... */ }5 6const billingModule = $module({7  name: "billing",8  services: [PaymentService, InvoiceService],9});

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:

bash
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:

bash
[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  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:

typescript
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:

typescript
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:

typescript
 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:

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, 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:

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// }

Alepha has also a built-in graph visualization tool in alepha/devtools that shows module boundaries and dependencies.