alepha@docs:~/docs/concepts$
cat 4-modules.md
2 min read
Last commit:

#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.

ts
 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.

ts
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.

ts
 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?

  1. Zero-Latency Local Calls: When both modules are running in the same process (the Monolith), Alepha detects this. The $client call effectively becomes a direct function call. There is no HTTP overhead.
  2. Location Transparency: If your app grows and you decide to move 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.
  3. 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.