#Modules
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.
#Defining a Module
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);
#The Golden Rule: Boundaries
Here is where Alepha differs from other tools.
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.
#How to communicate between modules
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}
#Why do it this way?
- Zero-Latency Local Calls: When both modules are running in the same process (the Monolith), Alepha detects this. The
$clientcall effectively becomes a direct function call. There is no HTTP overhead. - Location Transparency: If your app grows and you decide to move
NotificationModuleto a separate server (Microservices), you don't change a single line of code inUserService. You just configure via$remotefor the discovery. - Type Safety: You still get full TypeScript autocomplete and validation for the remote module, because TypeBox schemas are shared.
#Summary
| 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.