When you are building a "Hello World" app, you can put everything in main.ts.
When you are building a SaaS platform, that approach turns into spaghetti code before lunch.
Modules are Alepha's tool for organizing code into logical, decoupled domains. They allow you to build a Modular Monolith: an application that runs as a single process but is structured like a set of independent microservices.
A module is simply a container that groups related Services, Providers, and Primitives. You define one using the $module primitive.
1import { $module } from "alepha"; 2import { UserController } from "./UserController"; 3import { UserRepository } from "./UserRepository"; 4 5export const UserModule = $module({ 6 // Namespaces are important for logging and debugging 7 name: "app.users", 8 9 // Register all services belonging to this domain10 services: [11 UserController,12 UserRepository13 ]14});
Then, you register the module in your main entry point.
1import { Alepha, run } from "alepha";2import { UserModule } from "./modules/users";3import { BillingModule } from "./modules/billing";4 5const app = Alepha.create()6 .with(UserModule)7 .with(BillingModule);8 9run(app);
Here is where Alepha differs from other frameworks.
In a traditional Node.js app, it is tempting to just import BillingService into UserService and use it.
Don't do this.
If Module A directly $injects a service from Module B, you have created a tight coupling. You cannot split them later. You cannot deploy them separately. You have created a "Distributed Monolith" worst-case scenario.
Alepha enforces a strict boundary using the Links system.
Instead of injecting the instance of another module's service, you create a Client that talks to its API surface.
1import { $client } from "alepha/server/links"; 2import type { NotificationApi } from "../notifications/NotificationApi"; 3 4class UserService { 5 // ❌ BAD: Tight coupling 6 // notifications = $inject(NotificationService); 7 8 // ✅ GOOD: Loose coupling via API contract 9 notifications = $client<NotificationApi>();10 11 createUser = $action({12 handler: async ({ body }) => {13 // Create the user...14 15 // Call the other module16 // -> if both modules are local, this is a direct call17 // -> if remote, this is an remote call18 await this.notifications.sendWelcomeEmail({ email: body.email });19 }20 });21}
$client call effectively becomes a direct function call. There is no HTTP overhead.NotificationModule to a separate server (Microservices), you don't change a single line of code in UserService. You just configure via $remote for the discovery.| Feature | Use $inject |
Use $client |
|---|---|---|
| Scope | Inside the same module. | Across different modules. |
| Performance | Direct memory reference. | Optimized proxy (direct call or HTTP). |
| Coupling | High (Dependencies are hard-linked). | Low (Contract-based communication). |
| Use Case | Service logic, Database Repositories. | communicating between domains (e.g., Billing -> Users). |
Think of Modules as "Mini Applications" living together. Treat them with respect, keep their boundaries clean, and your codebase will remain maintainable for years.